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])
@bdarcus
Copy link

bdarcus commented Oct 15, 2019

They made another very minor change that broke the code. They placed these comments at top of the file.

# --------------------------------------
# 2019-10-15
# FR : Le champ Hypnogramme contient une valeur de stade de sommeil par période de 30 secondes. REM correspond au sommeil paradoxal, Light au sommeil léger, Deep au sommeil profond, Wake à l'éveil, N/A à l'absence de données
# EN : Hypnogram contains one sleep stage value per 30-seconds period. N/A stands for missing data.
# 
#  --------------------------------------

@bdarcus
Copy link

bdarcus commented Oct 15, 2019

This line fixes it.

        reader = DictReader(filter(lambda row: row[0]!='#', fh_r), delimiter=';')

@bdarcus
Copy link

bdarcus commented Oct 15, 2019

Oh, and per the comment they added, seems "N/A" is what they use to denote the empty value. I've updated my fork accordingly.

@jeremyblow
Copy link
Author

@bdarcus, great, I've merged in your updates. Learned two things today:

  1. gists have push/pull/merge capabilities.
  2. # as comments in CSVs, while outside RFC, are not unconventional in engineering.

@shafqatevo
Copy link

Great work, gentlemen! Will use this soon...

@lucasmafaldo
Copy link

Hi,

Sorry for the possible dumb question (I am not a programmer), but I am getting the following error after I try using "python dreem_csv_to_oscar_zeo.py data_in.csv data_out.csv":
File "dreem_csv_to_oscar_zeo.py", line 94
print(f"IN: {dict(in_row)}"

Am I doing something wrong?

Thank you!

@bdarcus
Copy link

bdarcus commented Nov 2, 2019 via email

@bdarcus
Copy link

bdarcus commented Nov 2, 2019

Other obvious question is did you specify the right input filename?

Dreem defaults to "export_data.csv", so correct command should be:

python dreem_csv_to_oscar_zeo.py export_data.csv data_out.csv

@lucasmafaldo
Copy link

I was not using Python 3. I should have checked this (sorry -- it's been a few years since I used a command line).

I am getting another error now. My source file is "data.csv". I apologise if I am still missing something basic.

python3 dreem_csv_to_oscar_zeo.py data.csv data_out.csv
Traceback (most recent call last):
File "dreem_csv_to_oscar_zeo.py", line 98, in
convert(argv[1], argv[2])
File "dreem_csv_to_oscar_zeo.py", line 90, in convert
writer = DictWriter(fh_w, delimiter=',', fieldnames=translate_row())
File "dreem_csv_to_oscar_zeo.py", line 55, in translate_row
stages = hypnogram_to_stages(row.get("Hypnogram"))
File "dreem_csv_to_oscar_zeo.py", line 51, in hypnogram_to_stages
return [stage_map.get(value.lower()) for value in hypnogram[1:-1].split(',')]
TypeError: 'NoneType' object is not subscriptable

@jeremyblow
Copy link
Author

@lucasmafaldo, as bdarcus suggested, that specific error is due to using Python 2 instead of Python 3. That said, I went ahead updated the script so both Python versions are supported.

I also corrected an issue with a misnamed variable.

@jeremyblow
Copy link
Author

@lucasmafaldo, your last error was because of a bug corrected in the latest revision. Please try again.

@lucasmafaldo
Copy link

Thank you -- both for the help and the script! It is working perfectly!

@jeremyblow
Copy link
Author

Super! Thanks for the feedback. Sweet dreams.

@bdarcus
Copy link

bdarcus commented Dec 31, 2019

@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.

@bdarcus
Copy link

bdarcus commented Jan 6, 2020

I addressed the timezone issue in my fork. Not sure it's the best way, but it seems to work.

@bdarcus
Copy link

bdarcus commented Feb 2, 2020

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:

https://gitlab.com/CPAPreporter/oscar-code/blob/master/oscar/SleepLib/loader_plugins/dreem_loader.cpp

@jeremyblow
Copy link
Author

@bdarcus, good! Looks like our little widget has served its purpose. Thanks for the help!

@rpfile
Copy link

rpfile commented Feb 25, 2024

@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

@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