Last active
December 14, 2016 17:05
-
-
Save remomueller/d502c03307e13214d9c918b27f2567e7 to your computer and use it in GitHub Desktop.
Simple script for loading and retrieving information from annotation files.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
# gem install xml-simple --no-document | |
# ruby loading_annotations.rb | |
require 'rubygems' | |
require 'csv' | |
require 'xmlsimple' | |
# Take the XML files in this folder `/annotation-testing` and answer two | |
# questions: | |
# 1) How many epochs/minutes was the subject asleep? | |
# 2) How many obstructive apnea events were marked on the recording? | |
# Asleep is defined as SleepStage = 1,2,3,4,5 | |
AWAKE_STAGES = [0] | |
SLEEP_STAGES = [1, 2, 3, 4, 5] | |
NREM_STAGES = [1, 2, 3, 4] | |
REM_STAGES = [5] | |
# Simplifies filtering an array of objects by attributes. | |
class Array | |
def where(filters) | |
results = dup | |
filters.each do |method, value| | |
if value.is_a?(Array) | |
results.select! { |object| value.include?(object.send(method)) } | |
else | |
results.select! { |object| object.send(method) == value } | |
end | |
end | |
results | |
end | |
end | |
# Represents a single scored event in an EDF, along with information present in | |
# the signal at the location of the event. | |
class ScoredEvent | |
attr_accessor :name, :lowest_spo2, :desaturation, :start, :duration, :input, | |
:stage | |
def initialize(hash = {}) | |
@name = hash[:name] | |
@lowest_spo2 = hash[:lowest_spo2] | |
@desaturation = hash[:desaturation] | |
@start = hash[:start] | |
@duration = hash[:duration] | |
@input = hash[:input] | |
@stage = nil | |
end | |
def set_stage(sleep_stages, epoch_length) | |
return if @start.nil? | |
index = (@start / epoch_length).floor | |
@stage = sleep_stages[index] | |
end | |
end | |
# Represents a set of Annotation of an EDF, including sleep stages, epoch | |
# length, and scored events. | |
class Annotation | |
attr_accessor :filename, :xml, :epoch_length, :sleep_stages, :scored_events | |
def initialize(filename) | |
@filename = filename | |
@xml = XmlSimple.xml_in(filename) | |
@epoch_length = parse_epoch_length | |
@sleep_stages = parse_sleep_stages | |
@scored_events = parse_scored_events | |
end | |
private | |
def parse_epoch_length | |
@xml['EpochLength'][0].to_i | |
end | |
def parse_sleep_stages | |
(@xml['SleepStages'][0]['SleepStage'] || []).collect(&:to_i) | |
end | |
def parse_scored_events | |
(@xml['ScoredEvents'][0]['ScoredEvent'] || []).collect do |hash| | |
hash = { | |
name: extract(hash, 'Name'), | |
lowest_spo2: extract(hash, 'LowestSpO2', convert: :to_f), | |
desaturation: extract(hash, 'Desaturation', convert: :to_f), | |
start: extract(hash, 'Start', convert: :to_f), | |
duration: extract(hash, 'Duration', convert: :to_f), | |
input: extract(hash, 'Input') | |
} | |
scored_event = ScoredEvent.new(hash) | |
scored_event.set_stage(@sleep_stages, @epoch_length) | |
scored_event | |
end | |
end | |
def extract(hash, key, convert: nil) | |
value = hash[key] ? hash[key][0] : nil | |
convert && value ? value.send(convert) : value | |
end | |
end | |
def annotations(recursive: true) | |
return enum_for(:annotations, recursive: recursive) unless block_given? | |
annotation_paths(recursive: recursive).each do |file_path| | |
yield Annotation.new(file_path) | |
end | |
end | |
def annotation_paths(recursive: true) | |
path = "#{'**/' if recursive}*.xml" | |
Dir.glob(path, File::FNM_CASEFOLD) | |
end | |
total_annotations = annotation_paths.count | |
csv_path = 'annotations.csv' | |
CSV.open(csv_path, 'wb') do |csv| | |
csv << [ | |
'Annotation', 'Asleep (min)', 'Recording Time (min)', | |
'OAs', 'REM OAs', 'NREM OAs', | |
'CAs', 'REM CAs', 'NREM CAs', | |
'HYs', 'REM HYs', 'NREM HYs' | |
] | |
annotations.each_with_index do |annotation, index| | |
asleep_stages = annotation.sleep_stages.where(itself: SLEEP_STAGES) | |
minutes_asleep = asleep_stages.count * annotation.epoch_length / 60.0 | |
minutes_total = annotation.sleep_stages.count * annotation.epoch_length / 60.0 | |
oa_events = annotation.scored_events.where(name: 'Obstructive Apnea') | |
rem_oa_events = oa_events.where(stage: REM_STAGES) | |
nrem_oa_events = oa_events.where(stage: NREM_STAGES) | |
ca_events = annotation.scored_events.where(name: 'Central Apnea') | |
rem_ca_events = ca_events.where(stage: REM_STAGES) | |
nrem_ca_events = ca_events.where(stage: NREM_STAGES) | |
hy_events = annotation.scored_events.where(name: 'Hypopnea') | |
rem_hy_events = hy_events.where(stage: REM_STAGES) | |
nrem_hy_events = hy_events.where(stage: NREM_STAGES) | |
# puts message | |
print "\rFile #{index + 1} of #{total_annotations}: #{annotation.filename} " | |
csv << [ | |
annotation.filename, minutes_asleep, minutes_total, | |
oa_events.count, rem_oa_events.count, nrem_oa_events.count, | |
ca_events.count, rem_ca_events.count, nrem_ca_events.count, | |
hy_events.count, rem_hy_events.count, nrem_hy_events.count | |
] | |
end | |
end | |
puts "\nDONE" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment