Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/sleepbouts #157

Merged
merged 4 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 50 additions & 15 deletions pyActigraphy/filters/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def create_inactivity_mask(self, duration, threshold=1):
# Create actual mask
self.mask = _create_inactivity_mask(self.raw_data, nepochs, threshold)

def add_mask_period(self, start, stop):
def add_mask_period(self, start, stop, error='raise'):
""" Add a period to the inactivity mask

Parameters
Expand All @@ -131,8 +131,21 @@ def add_mask_period(self, start, stop):
Start time (YYYY-MM-DD HH:MM:SS) of the inactivity period.
stop: str
Stop time (YYYY-MM-DD HH:MM:SS) of the inactivity period.
error: str
If set to 'raise', raise a ValueError whenever the mask
period is not within the recording's start/stop times.
If set to 'warn', simply issue a warning.
Available options are: ['raise','warn']. Default is 'raise'.
"""

error_options = ['raise', 'warn']
if error not in error_options:
raise ValueError(
(
"Error option ({}) is not available.\n".format(error)
+"Available options are: {}.".format(",".join(error_options))
)
)
# Check if a mask has already been created
# NB : if the inactivity_length is not None, accessing the mask will
# trigger its creation.
Expand All @@ -145,24 +158,41 @@ def add_mask_period(self, start, stop):

# Check if start and stop are within the index range
if (pd.Timestamp(start) < self.mask.index[0]):
raise ValueError((
"Attempting to set the start time of a mask period before "
+ "the actual start time of the data.\n"
+ "Mask start time: {}".format(start)
+ "Data start time: {}".format(self.mask.index[0])
))
if error == 'raise':
raise ValueError((
"Attempting to set the start time of a mask period before "
+ "the actual start time of the data:\n"
+ "- Mask start time: {}\n".format(start)
+ "- Data start time: {}".format(self.mask.index[0])
))
else:
print((
"Attempting to set the start time of a mask period before "
+ "the actual start time of the data:\n"
+ "- Mask start time: {}\n".format(start)
+ "- Data start time: {}".format(self.mask.index[0])
))

if (pd.Timestamp(stop) > self.mask.index[-1]):
raise ValueError((
"Attempting to set the stop time of a mask period after "
+ "the actual stop time of the data.\n"
+ "Mask stop time: {}".format(stop)
+ "Data stop time: {}".format(self.mask.index[-1])
))
if error == 'raise':
raise ValueError((
"Attempting to set the stop time of a mask period after "
+ "the actual stop time of the data:\n"
+ "- Mask stop time: {}\n".format(stop)
+ "- Data stop time: {}".format(self.mask.index[-1])
))
else:
print((
"Attempting to set the stop time of a mask period after "
+ "the actual stop time of the data:\n"
+ "- Mask stop time: {}\n".format(stop)
+ "- Data stop time: {}".format(self.mask.index[-1])
))

# Set mask values between start and stop to zeros
self.mask.loc[start:stop] = 0

def add_mask_periods(self, input_fname, *args, **kwargs):
def add_mask_periods(self, input_fname, error='raise', *args, **kwargs):
""" Add periods to the inactivity mask

Function to read start and stop times from a Mask log file. Supports
Expand All @@ -172,6 +202,11 @@ def add_mask_periods(self, input_fname, *args, **kwargs):
----------
input_fname: str
Path to the log file.
error: str
If set to 'raise', raise a ValueError whenever the mask
period is not within the recording's start/stop times.
If set to 'warn', simply issue a warning.
Available options are: ['raise','warn']. Default is 'raise'.
*args
Variable length argument list passed to the subsequent reader
function.
Expand All @@ -185,4 +220,4 @@ def add_mask_periods(self, input_fname, *args, **kwargs):

# Iterate over the rows of the DataFrame
for _, row in log.iterrows():
self.add_mask_period(row['Start_time'], row['Stop_time'])
self.add_mask_period(row['Start_time'], row['Stop_time'], error=error)
5 changes: 3 additions & 2 deletions pyActigraphy/sleep/scoring/roenneberg.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import numpy as np
import pandas as pd
from .utils import consecutive_values, correlation_series
from .utils import find_highest_peak_idx
from .utils import find_first_peak_idx


def _extract_trend(data, period='24h', min_period='12h', closed='right'):
Expand Down Expand Up @@ -170,7 +170,8 @@ def _clean_sleep_bout(

# Find the date_time index corresponding to the highest correlation peak
n_succ = int(pd.Timedelta(r_consec_below)/uncleaned_binary_data.index.freq)
sleep_offset_idx = find_highest_peak_idx(corr, n_succ=n_succ+1)
#sleep_offset_idx = find_highest_peak_idx(corr, n_succ=n_succ+1)
sleep_offset_idx = find_first_peak_idx(corr, n_succ=n_succ+1)

if sleep_offset_idx is not None:
return uncleaned_binary_data.index[sleep_offset_idx]
Expand Down
44 changes: 44 additions & 0 deletions pyActigraphy/sleep/scoring/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,50 @@ def is_a_peak(x):
return np.all(x[0] > x[1:])


def find_first_peak_idx(x, n_succ=3):
r'''Find the index of the first peak.

A peak is defined as an element whose value is higher than those of the
successive Nth elements.

Parameters
----------
x : np.ndarray
Array containing the peak candidates.

n_succ : int, optional
Number of successive elements to consider when searching for a peak.
Default is 3.

Return
----------
idx : int
Index of the first peak.


Notes
-----
When several peaks are found, the index of the first
peak is returned.

'''

# Check if input is long enough to create windows
if n_succ > x.shape[0]:
return None

peak_candidate_idx, = np.apply_along_axis(
is_a_peak,
axis=1,
arr=rolling_window(x, n_succ)
).nonzero()

if(len(peak_candidate_idx) > 0):
return peak_candidate_idx[0]
else:
return None


def find_highest_peak_idx(x, n_succ=3):
r'''Find the index of the highest peak.

Expand Down
29 changes: 27 additions & 2 deletions pyActigraphy/sleep/sleep.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from ..utils.filters import filter_ts_duration
import numpy as np


class SleepBoutMixin(object):
Expand All @@ -8,6 +9,7 @@ def sleep_bouts(
self,
duration_min=None,
duration_max=None,
verbose=False,
algo='Roenneberg',
*args, **kwargs
):
Expand All @@ -23,6 +25,9 @@ def sleep_bouts(
duration_max: str,optional
Maximal time duration for a sleep period.
Default is None (no filtering).
verbose: bool,optional
If set to True, display informations (start,end) about detected sleep bouts.
Default is False.
algo: str, optional
Sleep/wake scoring algorithm to use.
Default is 'Roenneberg'.
Expand Down Expand Up @@ -54,10 +59,30 @@ def sleep_bouts(

# For each inactivity period (from offset to onset times)
sleep_bouts = []
for onset, offset in zip(onsets, offsets):
sleep_bout = self.data[offset:onset]
for offset in offsets:
#[(pd.Series(aont) > aofft[0]).idxmax()]
# Select candidates for the associated onset time
is_onset_later = onsets>offset
# If none was found, skip this offset time and warn users
if((is_onset_later==False).all()):
if verbose:
print(
"Could not find any onset time past "
+ "the current offset time ({}).".format(offset)
+ "\nSkipping current offset time."
)
continue
# Search forward for its closest onset time: return its index
idx = np.argmax(is_onset_later)

sleep_bout = self.data.loc[offset:onsets[idx]]
sleep_bouts.append(sleep_bout)

# sleep_bouts = []
# for onset, offset in zip(onsets, offsets):
# sleep_bout = self.data[offset:onset]
# sleep_bouts.append(sleep_bout)

return filter_ts_duration(sleep_bouts, duration_min, duration_max)

def active_bouts(
Expand Down