AI | ML/Sleep Analysis

Sleep-EDF 데이터셋 정리 및 추출

깜태 2021. 9. 30. 15:04
728x90

Sleep-EDF 데이터셋은 수면다원검사(Polysomnography, PSG)라고 하여

흔히 병원에서 수면검사를 받을 때 아래와 같은 수많은 센서를 붙여 수면 시간동안 신체정보를 받는데,

이를 기록해둔 오픈데이터셋이다.

데이터셋은 https://physionet.org/content/sleep-edfx/1.0.0/ 홈페이지에서 받을 수 있고, 8.1GB 정도의 용량이다.

받고 나면 sleep-cassette, sleep-telemetry 라는 폴더를 볼 수 있는데,

홈페이지 설명을 빌리자면 cassette는 카세트, 시간순으로 쭉 기록한 데이터고,

Telemetry는 테마제팜(temazepam)이라는 불면증 치료제 효과를 확인하기 위해 만든 데이터셋이라고 한다.

 

개인적으로는 카세트 데이터를 쓸 것으로 Telemetry에 대한 설명은 생략한다.

카세트 데이터는 PSG 타입과 Hypnogram 타입으로 나뉘는데,

PSG는 센서 데이터, Hypnogram은 수면단계를 [W, 1, 2, 3, 4, R, M, ? ]으로 분류한 것이다.

 

위의 PSG 그림을 보면 알겠지만 센서 데이터가 매우 많은 만큼 쓰고 싶은 데이터를 골라서 쓰면 되는데,

이게 글을 쓰게 된 이유이다.

 

설치

Python에서 EDF 파일을 읽는 방법은 pyEDFlib 을 설치하면 된다.

pip install pyEDFlib

 

사용법

read_edf 함수를 통해 간단하게 파일을 불러올 수 있고, 간단하게 구현하면 아래와 같다.

from pyedflib import highlevel
import numpy as np
import glob

path = '[my_path]/sleep-edfx/1.0.0/sleep-cassette'
data_list = glob.glob(path+'/**')
trains = [x for x in data_list if x.endswith('PSG.edf')]
labels = [x for x in data_list if x.endswith('Hypnogram.edf')]
print(highlevel.read_edf(trains[0]))
print(highlevel.read_edf(labels[0]))

read_edf 함수의 리턴 값은 3종류로 signal, signal_header, header 이다.

프린트 함수로 확인해보면 PSG 데이터의 경우 signal 값을 볼 수 있지만,

Hypnogram 은 수면단계를 분류한 데이터이므로 센서값 자체는 없다.

 

signal_header를 뜯어보면 len()을 통해 각 데이터들의 개수를 확인해보면 센서 데이터의 종류는 7개로 표시하면 아래와 같다.

[EEG Fpz-Cz, EEG Pz-Oz, EOG horizontal, Resp oro-nasal, EMG submental, Temp rectal, Event Marker]

[
   {
      "label":"EEG Fpz-Cz",
      "dimension":"uV",
      "sample_rate":100.0,
      "physical_max":209.0,
      "physical_min":-208.0,
      "digital_max":2047,
      "digital_min":-2048,
      "prefilter":"HP:0.5Hz LP:100Hz [enhanced cassette BW]",
      "transducer":"Ag-AgCl electrodes"
   },
   {
      "label":"EEG Pz-Oz",
      "dimension":"uV",
      "sample_rate":100.0,
      "physical_max":202.0,
      "physical_min":-200.0,
      "digital_max":2047,
      "digital_min":-2048,
      "prefilter":"HP:0.5Hz LP:100Hz [enhanced cassette BW]",
      "transducer":"Ag-AgCl electrodes"
   },
   {
      "label":"EOG horizontal",
      "dimension":"uV",
      "sample_rate":100.0,
      "physical_max":988.0,
      "physical_min":-987.0,
      "digital_max":2047,
      "digital_min":-2048,
      "prefilter":"HP:0.5Hz LP:100Hz [enhanced cassette BW]",
      "transducer":"Ag-AgCl electrodes"
   },
   {
      "label":"Resp oro-nasal",
      "dimension":"",
      "sample_rate":1.0,
      "physical_max":32767.0,
      "physical_min":-32768.0,
      "digital_max":32767,
      "digital_min":-32768,
      "prefilter":"HP:0.03Hz LP:0.9Hz",
      "transducer":"Oral-nasal thermistors"
   },
   {
      "label":"EMG submental",
      "dimension":"uV",
      "sample_rate":1.0,
      "physical_max":5.0,
      "physical_min":-5.0,
      "digital_max":2500,
      "digital_min":-2500,
      "prefilter":"HP:16Hz Rectification LP:0.7Hz",
      "transducer":"Ag-AgCl electrodes"
   },
   {
      "label":"Temp rectal",
      "dimension":"DegC",
      "sample_rate":1.0,
      "physical_max":40.0,
      "physical_min":34.0,
      "digital_max":3144,
      "digital_min":-3234,
      "prefilter":"",
      "transducer":"Rectal thermistor"
   },
   {
      "label":"Event marker",
      "dimension":"",
      "sample_rate":1.0,
      "physical_max":2048.0,
      "physical_min":-2047.0,
      "digital_max":2048,
      "digital_min":-2047,
      "prefilter":"Hold during 2 seconds",
      "transducer":"Marker button"
   }
]

위의 label 이름을 보고 각각이 어떤 센서인지 알았으니,

쓰고 싶은 센서를 골라서 signal 데이터를 고르면 된다.

예를 들어 "Resp oro-nasal" 센서가 쓰고싶다면 위의 signals[3]을 하면 값을 볼 수 있고

간단하게 표시하면 아래와 같다.

 

print(signals[3], signals[3].shape, signal_header[3])

[-627.  619. -520. ...  170.  144.  177.] 
(84900,) 
{'label': 'Resp oro-nasal', 'dimension': '', 'sample_rate': 1.0, 'physical_max': 32767.0, 'physical_min': -32768.0, 'digital_max': 32767, 'digital_min': -32768, 'prefilter': 'HP:0.03Hz LP:0.9Hz', 'transducer': 'Oral-nasal thermistors'}

 

이렇게 보았을 때, 길이는 84900나 되는 센서 데이터를 획득하였고, sample_rate를 보면 1.0인 것으로 보아

1초당 1개의 데이터로 즉 84900초의 데이터라는 말이 되고, 이는 23.58시간의 데이터라는 것을 볼 수 있다.

 

다음으로 Hypnogram 데이터에서도 위와 같이 똑같이 진행하면 아래와 같이 나온다.

(array([], dtype=float64), [],
{
   "technician":"",
   "recording_additional":"",
   "patientname":"Female 33yr",
   "patient_additional":"",
   "patientcode":"",
   "equipment":"",
   "admincode":"",
   "gender":"Female",
   "startdate":datetime.datetime(1989,4,25,14,50),
   "birthdate":"",
   "annotations":[[0.0, b'26070', 'Sleep stage W'], 
   		[26070.0, b'90', 'Sleep stage 1'], 
   		[26160.0, b'510', 'Sleep stage 2']
        ...
        ]
})

signal 값과 signal_header에 대한 값은 비어있는 것을 볼 수 있고, header에 대한 데이터만 존재하는 것을 볼 수 있다.

annotations를 자세히 보면 0에서 시작해 26060, sleep stage w 라고 되어있는데,

이 부분에서는 자료를 못 찾아 곰곰히 보다보니 [시작시간, 기간, 단계] 라는 관계를 볼수 있었다.

다시 말하면 0초에서 시작해서 '26060'초 동안은 'Sleep stage W' 라는 의미이다.

추가로 sleep stage 내부에는 ?라는 값도 존재하는데, 이는 기록이 되지 않은 것으로 유저의 선택으로 남겨놓았는데 개인적으로는 그냥 안쓰는 게 나을 것 같아 제외해보았다.

 

마지막으로 모든 데이터에 대해 추출 뒤에 %로 나누어서 matplotlib으로 띄어보았다.

일반적으로 수면 비율은 아래와 같은 그래프와 비슷한 양상을 가지고,

대체로 수면의 50~60%가 얕은 잠, 10~15%가 깊은 잠, 20~25%가 렘수면이라고 한다.

수면 단계 비율이 맞는지 보기 위해 일어나있거나, 수면에 해당되는 상태인 [1,2,3,4,r] 에 대해 정규화하여 출력해보았다.

{
   "stage 1":0.16615712432833055,
   "stage 2":0.5337224383916991,
   "stage 3":0.06788493607559755,
   "stage 4":0.03278055709962325,
   "stage R":0.19945494410474957
}

 

 

데이터를 살펴보면 1~2단계를 합쳐 얕은 잠으로 보면 68%, 3~4단계를 깊은잠으로 보면 0.09, 렘수면은 0.19로 나와 전반적인 기준보다는 잠을 얕게 자는 것으로 보인다.

 

오늘은 이렇게 EDF 데이터셋에 대해 간단하게 다운받고 조사해보았다.

 

참고 : https://dydeeplearning.tistory.com/15

 

 

 

 

 

 

728x90