Source code for circstudio.analysis.sleep.diary

import os
import pandas as pd
import pyexcel as pxl
from .sleep import *


[docs] class SleepDiary: """ Class for reading sleep diaries. References ---------- This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License. Original author: Grégory Hammad (gregory.hammad@uliege.be). [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021). pyActigraphy: Open-source python package for actigraphy data visualization and analysis. PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514 [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters. LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863 """ def __init__( self, input_fname, start_time, periods, frequency, header_size=2, state_index=None, state_colour=None, ): # Set the default state index and color if not specified by the user if state_index is None: state_index = {"ACTIVE": 2, "NAP": 1, "NIGHT": 0, "NOWEAR": -1} if state_colour is None: state_colour = {"NAP": "#7bc043", "NIGHT": "#d3d3d3", "NOWEAR": "#ee4035"} # Get absolute file path input_fname = os.path.abspath(input_fname) sd_array = pxl.get_array(file_name=input_fname) self._name = sd_array[0][1] self._diary = pd.DataFrame( sd_array[header_size + 1 :], columns=sd_array[header_size] ).astype({"TYPE": "str", "START": "datetime64[ns]", "END": "datetime64[ns]"}) # Inplace drop of useless columns self._diary.drop(columns=["DURATION (min)"], inplace=True, errors="ignore") # Inplace drop of NA self._diary.dropna(inplace=True) self._state_index = state_index self._state_colour = state_colour # Create a time series with ACTIVE as default value. self._raw_data = pd.Series( data=self._state_index["ACTIVE"], index=pd.date_range(start_time, periods=periods, freq=frequency), dtype=int, ) # Replace the default value with the ones found in the sleep diary. for index, row in self._diary.iterrows(): self._raw_data[row["START"] : row["END"]] = self._state_index[row["TYPE"]] # Create a template shape to overlay over a plotly plot self._shaded_area = dict( type="rect", xref="x", yref="paper", x0=0, y0=0, x1=1, y1=1, fillcolor="", opacity=0.5, layer="below", line=dict(width=0), ) def __str__(self): return self._diary.to_string() def __call__(self): return self._diary @property def name(self): """The name of the subject.""" return self._name @property def diary(self): """The dataframe containing the data found in the sleep diary.""" return self._diary @property def state_index(self): """The indices assigned to the states found in the sleep diary.""" return self._state_index @state_index.setter def state_index(self, value): self._state_index = value @property def state_colour(self): """The colours assigned to the states found in the sleep diary.""" return self._state_colour @state_colour.setter def state_colour(self, value): self._state_colour = value @property def raw_data(self): """The time series related to the states found in the sleep diary.""" return self._raw_data @property def shaded_area(self): """The template shape which can be overlaid over a plotly plot of the associated actimetry time series.""" return self._shaded_area @shaded_area.setter def shaded_area(self, value): self._shaded_area = value
[docs] def shapes(self): """ """ shapes = [] for index, row in self._diary.iterrows(): shape = self._shaded_area.copy() shape["x0"] = row["START"] shape["x1"] = row["END"] shape["fillcolor"] = self._state_colour[row["TYPE"]] shapes.append(shape) return shapes
[docs] def summary(self): """Returns a dataframe of summary statistics.""" if "DURATION" not in self._diary.columns: self._diary["DURATION"] = self._diary["END"] - self._diary["START"] return self._diary.groupby(["TYPE"])["DURATION"].describe()
[docs] def state_infos(self, state): """Returns summary statistics for a given state Parameters ---------- state: str State of interest Returns ------- mean: pd.Timedelta Mean duration of the required state. std: pd.Timedelta Standard deviation of the durations of the required state. References ---------- This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License. Original author: Grégory Hammad (gregory.hammad@uliege.be). [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021). pyActigraphy: Open-source python package for actigraphy data visualization and analysis. PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514 [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters. LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863 """ # Re-use the summary function summary = self.summary() # Verify that the state is present in the summary object if state not in summary.index: raise KeyError( "{} is not a valid state. Valid states are {}".format( state, '" or "'.join(summary.index) ) ) # Access the summary object to get the mean mean = summary.loc[state, "mean"] # Access the summary object to get the std std = summary.loc[state, "std"] return mean, std
[docs] def total_bed_time(self, state="NIGHT"): """Returns the total in-bed time Parameters ---------- state : str, optional State of interest. Default is 'NIGHT'. Returns ------- mean: pd.Timedelta Mean duration of the required state. std: pd.Timedelta Standard deviation of the durations of the required state. References ---------- This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License. Original author: Grégory Hammad (gregory.hammad@uliege.be). [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021). pyActigraphy: Open-source python package for actigraphy data visualization and analysis. PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514 [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters. LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863 """ return self.state_infos(state)
[docs] def total_nap_time(self, state="NAP"): """Returns the total nap time Parameters ---------- state : str, optional State of interest. Default is 'NAP'. Returns ------- mean: pd.Timedelta Mean duration of the required state. std: pd.Timedelta Standard deviation of the durations of the required state. References ---------- This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License. Original author: Grégory Hammad (gregory.hammad@uliege.be). [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021). pyActigraphy: Open-source python package for actigraphy data visualization and analysis. PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514 [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters. LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863 """ return self.state_infos(state)
[docs] def total_nowear_time(self, state="NOWEAR"): """Returns the total 'no-wear' time Parameters ---------- state : str, optional State of interest. Default is 'NOWEAR'. Returns ------- mean: pd.Timedelta Mean duration of the required state. std: pd.Timedelta Standard deviation of the durations of the required state. References ---------- This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License. Original author: Grégory Hammad (gregory.hammad@uliege.be). [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021). pyActigraphy: Open-source python package for actigraphy data visualization and analysis. PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514 [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters. LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863 """ return self.state_infos(state)
[docs] def sleep_efficiency(self, data): """ Computes sleep efficiency as the average total sleep time, as classified by the Roenneberg algorithm, divided by the average total sleep time, as identified in the sleep diary. Parameters ---------- data : pd.Series Returns ------- float Sleep efficiency (decimal) """ # Calculate average total sleep time (within the main sleep bout) avg_total_sleep_time = main_sleep_bouts(data=data)[1] # Calculate average total bedtime (from sleep diary) avg_total_bed_time = self.total_bed_time()[0] # If avg_total_bed_time is zero, do not return a result if avg_total_bed_time == 0: warnings.warn("Average total sleep time is 0.") return None # If avg_total_bed_time < avg_total_sleep_time if avg_total_sleep_time > avg_total_bed_time: warnings.warn( "Average total sleep time is greater than average total sleep time." ) return None return avg_total_sleep_time / avg_total_bed_time
[docs] def sleep_onset_latency(self, data): """ Computes sleep onset latency using the Roenneberg algorithm to predict sleep onset and the sleep diary to determine total bedtime. Parameters ---------- data : pandas.Series Input data series with a DatetimeIndex, where the index specifies the time points and the values represent the input variable (e.g., activity, light). Time and value arrays are extracted from this series. Returns ------- pd.Series Array containing sleep onset latency indexed by day of the recording. pd.Timedelta Mean sleep onset latency. """ main_sleep_df = main_sleep_bouts(data=data)[0] diary_nights_df = self._diary[self._diary["TYPE"] == "NIGHT"] # Create an empty dictionary to store sleep_onset_latency (sol) values sol = {} # Iterate over the rows of the sleep diary corresponding to nighttime for _, row in diary_nights_df.iterrows(): # Extract the date from the current row date = row["START"].date() # Identify matches between the sleep diary and detected periods of sleep matches = main_sleep_df[main_sleep_df["start_time"].dt.date == date] # If a match was found, then calculate the latency between bedtime and sleep onsets if not matches.empty: # Extract sleep onset sleep_onset = matches.iloc[0]["start_time"] # Calculate the latency and store it in the sol dictionary latency = sleep_onset - row["START"] # Return as missing value if predicted sleep time occurs before recorded bed time if latency < pd.Timedelta(0): latency = np.nan # Store the latency for the current date if latency is not np.nan: sol[date] = latency # Typecast and return, sol to a pd.Series, along with the mean sol = pd.Series(sol) return pd.Series(sol), np.mean(sol)
[docs] def plot(self, data): """Plot the sleep diary. References ---------- This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License. Original author: Grégory Hammad (gregory.hammad@uliege.be). [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021). pyActigraphy: Open-source python package for actigraphy data visualization and analysis. PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514 [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters. LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863 """ layout = go.Layout( title="Actigraphy data", xaxis=dict(title="Date time"), yaxis=dict(title="Counts/period"), shapes=self.shapes(), showlegend=False, ) fig = go.Figure(data=go.Scatter(x=data.index, y=data), layout=layout) return fig