Skip to content

Commit 1321d04

Browse files
committed
Implement sleep_efficiency and sleep_onset_latency in sleep diaries
1 parent 2287064 commit 1321d04

6 files changed

Lines changed: 276 additions & 212 deletions

File tree

circStudio/analysis/sleep/diary.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
22
import pandas as pd
33
import pyexcel as pxl
4+
import warnings
5+
from circStudio.analysis.sleep import *
46

57

68
class SleepDiary:
@@ -226,3 +228,85 @@ def total_nowear_time(self, state='NOWEAR'):
226228
"""
227229

228230
return self.state_infos(state)
231+
232+
233+
def sleep_efficiency(self, data):
234+
"""
235+
Computes sleep efficiency as the average total sleep time, as classified by the Roenneberg algorithm,
236+
divided by the average total sleep time, as identified in the sleep diary.
237+
238+
Parameters
239+
----------
240+
data : pd.Series
241+
242+
Returns
243+
-------
244+
float
245+
Sleep efficiency.
246+
247+
"""
248+
# Calculate average total sleep time (within the main sleep bout)
249+
avg_total_sleep_time = main_sleep_bouts(data=data)[1]
250+
251+
# Calculate average total bedtime (from sleep diary)
252+
avg_total_bed_time = self.total_bed_time()[0]
253+
254+
# If avg_total_bed_time is zero, do not return a result
255+
if avg_total_bed_time == 0:
256+
warnings.warn('Average total sleep time is 0.')
257+
return None
258+
259+
# If avg_total_bed_time < avg_total_sleep_time
260+
if avg_total_sleep_time > avg_total_bed_time:
261+
warnings.warn('Average total sleep time is greater than average total sleep time.')
262+
return None
263+
264+
return avg_total_sleep_time / avg_total_bed_time
265+
266+
267+
def sleep_onset_latency(self, data):
268+
"""
269+
Computes sleep onset latency using the Roenneberg algorithm to predict sleep onset and
270+
the sleep diary to determine total bedtime.
271+
272+
Parameters
273+
----------
274+
data : pandas.Series
275+
Input data series with a DatetimeIndex, where the index specifies the time points and
276+
the values represent the input variable (e.g., activity, light). Time and value arrays
277+
are extracted from this series.
278+
279+
Returns
280+
-------
281+
pd.Series
282+
Array containing sleep onset latency indexed by day of the recording.
283+
pd.Timedelta
284+
Mean sleep onset latency.
285+
286+
"""
287+
main_sleep_df = main_sleep_bouts(data=data)[0]
288+
diary_nights_df = self._diary[self._diary['TYPE'] == 'NIGHT']
289+
290+
# Create an empty dictionary to store sleep_onset_latency (sol) values
291+
sol = {}
292+
293+
# Iterate over the rows of the sleep diary corresponding to nighttime
294+
for _, row in diary_nights_df.iterrows():
295+
# Extract the date from the current row
296+
date = row['START'].date()
297+
298+
# Identify matches between the sleep diary and detected periods of sleep
299+
matches = main_sleep_df[main_sleep_df['start_time'].dt.date == date]
300+
301+
# If a match was found, then calculate the latency between bedtime and sleep onsets
302+
if not matches.empty:
303+
# Extract sleep onset
304+
sleep_onset = matches.iloc[0]['start_time']
305+
306+
# Calculate the latency and store it in the sol dictionary
307+
latency = sleep_onset - row['START']
308+
sol[date] = latency
309+
# Typecast and return, sol to a pd.Series, along with the mean
310+
sol = pd.Series(sol)
311+
return pd.Series(sol), np.mean(sol)
312+

circStudio/analysis/sleep/sleep.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,7 +1212,7 @@ def SleepProfile(data, freq='15min', algo='Roenneberg', *args, **kwargs):
12121212
return sleep_prof.resample(freq).mean()
12131213

12141214

1215-
def SleepRegularityIndex(data, freq='15min', bin_threshold=None, algo='Roenneberg', *args, **kwargs):
1215+
def SleepRegularityIndex(data, bin_threshold=None, algo='Roenneberg', *args, **kwargs):
12161216
r""" Sleep regularity index
12171217
12181218
Likelihood that any two time-points (epoch-by-epoch) 24 hours apart are
@@ -1518,4 +1518,61 @@ def active_durations(data, duration_min=None, duration_max=None, algo='Roenneber
15181518
**kwargs
15191519
)
15201520

1521-
return [s.index[-1]-s.index[0] for s in filtered_bouts]
1521+
return [s.index[-1]-s.index[0] for s in filtered_bouts]
1522+
1523+
1524+
def main_sleep_bouts(data, report='major'):
1525+
"""
1526+
Calculate main sleep episodes using the Roenneberg algorithm.
1527+
1528+
Parameters
1529+
----------
1530+
data : pandas.Series, optional
1531+
Input data series with a DatetimeIndex, where the index specifies the time points and
1532+
the values represent the input variable (e.g., activity, light). Time and value arrays
1533+
are extracted from this series.
1534+
report : str, optional
1535+
Either 'major' or 'minor'. Default is 'major'. If set to 'major', the function will
1536+
return a dataframe containing all the major sleep bouts in the recording, along
1537+
with the mean. If set to 'minor', the function will return a dataframe containing
1538+
all the minor sleep bouts in the recording, along with the mean.
1539+
1540+
Returns
1541+
-------
1542+
pd.DataFrame
1543+
Dataframe containing the main sleep episodes (date, start_time, stop_time and duration).
1544+
1545+
"""
1546+
# Compute the activity onset and off using the Roenneberg algorithm
1547+
activity_onset, activity_offset = Roenneberg_AoT(data)
1548+
1549+
# Create empty dataframe to store all the sleep events
1550+
sleep_events = pd.DataFrame()
1551+
1552+
# sleep_onset = activity_offset; sleep_offset = activity_onset
1553+
sleep_events['date'] = activity_onset.date
1554+
sleep_events['start_time'] = activity_offset
1555+
sleep_events['stop_time'] = activity_onset
1556+
1557+
# Sleep/rest episode duration
1558+
sleep_events['duration'] = sleep_events['stop_time'] - sleep_events['start_time']
1559+
1560+
# Identify main sleep episode
1561+
main_sleep = sleep_events.loc[sleep_events.groupby('date')['duration'].idxmax()]
1562+
1563+
# Identify minor sleep episodes
1564+
minor_sleep = sleep_events.drop(main_sleep.index)
1565+
1566+
if report == 'major':
1567+
# Calculate mean duration (in minutes) of the main sleep episode
1568+
mean = main_sleep['duration'].mean().total_seconds() / 60
1569+
1570+
# Return dataframe with major sleep events and summary stats
1571+
return main_sleep, mean
1572+
1573+
elif report == 'minor':
1574+
# Calculate mean duration (in minutes) of the main sleep episode
1575+
mean = minor_sleep['duration'].mean().total_seconds() / 60
1576+
1577+
# Return dataframe with major sleep events and summary stats
1578+
return minor_sleep, mean

docs/source/tutorial_3.ipynb

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@
3636
"cell_type": "code",
3737
"execution_count": 1,
3838
"id": "ba14333118837a04",
39-
"metadata": {},
39+
"metadata": {
40+
"ExecuteTime": {
41+
"end_time": "2025-07-23T14:16:41.434784Z",
42+
"start_time": "2025-07-23T14:16:39.106959Z"
43+
}
44+
},
4045
"outputs": [],
4146
"source": [
4247
"import circStudio\n",
@@ -6690,10 +6695,85 @@
66906695
},
66916696
{
66926697
"cell_type": "code",
6693-
"execution_count": null,
6698+
"execution_count": 4,
6699+
"id": "a02a7dde-08fe-4d1b-8401-c264de032916",
6700+
"metadata": {},
6701+
"outputs": [],
6702+
"source": [
6703+
"import pandas as pd\n",
6704+
"import numpy as np"
6705+
]
6706+
},
6707+
{
6708+
"cell_type": "code",
6709+
"execution_count": 96,
66946710
"id": "32e995ba-6cb9-411e-b4c5-ec531e865c1e",
66956711
"metadata": {},
66966712
"outputs": [],
6713+
"source": [
6714+
"def sleep_onset_latency():\n",
6715+
" main_sleep_df = pd.DataFrame({\n",
6716+
" 'date': ['1918-01-24', '1918-01-25', '1918-01-27'],\n",
6717+
" 'start_time': ['1918-01-24 23:43:00', '1918-01-25 22:17:00', '1918-01-27 01:43:00'],\n",
6718+
" 'stop_time': ['1918-01-25 06:25:00', '1918-01-26 07:20:00', '1918-01-27 07:11:00']\n",
6719+
" })\n",
6720+
" for col in main_sleep_df.columns:\n",
6721+
" main_sleep_df[col] = pd.to_datetime(main_sleep_df[col])\n",
6722+
"\n",
6723+
" diary_nights_df = pd.DataFrame({\n",
6724+
" 'START': ['1918-01-24 23:00:00', '1918-01-25 22:00:00', '1918-01-27 00:00:00'],\n",
6725+
" 'END': ['1918-01-25 07:00:00', '1918-01-26 07:30:00', '1918-01-27 07:30:00']\n",
6726+
" })\n",
6727+
" for col in diary_nights_df.columns:\n",
6728+
" diary_nights_df[col] = pd.to_datetime(diary_nights_df[col])\n",
6729+
"\n",
6730+
" # Create an empty dictionary to store sleep_onset_latency (sol) values\n",
6731+
" sol = {}\n",
6732+
" \n",
6733+
" # Iterate over the rows of the sleep diary corresponding to nighttime\n",
6734+
" for _, row in diary_nights_df.iterrows():\n",
6735+
" # Extract the date from the current row\n",
6736+
" date = row['START'].date()\n",
6737+
" matches = main_sleep_df[main_sleep_df['start_time'].dt.date == date]\n",
6738+
" if not matches.empty:\n",
6739+
" sleep_onset = matches.iloc[0]['start_time']\n",
6740+
" latency = sleep_onset - row['START']\n",
6741+
" sol[date] = latency\n",
6742+
" sol = pd.Series(sol)\n",
6743+
" return pd.Series(sol), np.mean(sol)"
6744+
]
6745+
},
6746+
{
6747+
"cell_type": "code",
6748+
"execution_count": 97,
6749+
"id": "5711a686-722d-4e11-9726-498beaed6e6a",
6750+
"metadata": {},
6751+
"outputs": [
6752+
{
6753+
"data": {
6754+
"text/plain": [
6755+
"(1918-01-24 0 days 00:43:00\n",
6756+
" 1918-01-25 0 days 00:17:00\n",
6757+
" 1918-01-27 0 days 01:43:00\n",
6758+
" dtype: timedelta64[ns],\n",
6759+
" Timedelta('0 days 00:54:20'))"
6760+
]
6761+
},
6762+
"execution_count": 97,
6763+
"metadata": {},
6764+
"output_type": "execute_result"
6765+
}
6766+
],
6767+
"source": [
6768+
"sleep_onset_latency()"
6769+
]
6770+
},
6771+
{
6772+
"cell_type": "code",
6773+
"execution_count": null,
6774+
"id": "ea5f75de-52d0-43c5-9252-43dac1698d96",
6775+
"metadata": {},
6776+
"outputs": [],
66976777
"source": []
66986778
}
66996779
],

tests/sleep_tests.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import pandas as pd
2+
import numpy as np
3+
4+
def main():
5+
sleep_onset_latency_test()
6+
7+
8+
def sleep_onset_latency_test():
9+
"""
10+
Computes sleep onset latency using the Roenneberg algorithm to predict sleep onset and
11+
the sleep diary to determine total bedtime.
12+
13+
Parameters
14+
----------
15+
16+
Returns
17+
-------
18+
pd.Series
19+
Sleep onset latency.
20+
21+
"""
22+
main_sleep_df = pd.DataFrame({
23+
'date': ['1918-01-24', '1918-01-25', '1918-01-27'],
24+
'start_time': ['1918-01-24 23:43:00', '1918-01-25 22:17:00', '1918-01-27 01:43:00'],
25+
'stop_time': ['1918-01-25 06:25:00', '1918-01-26 07:20:00', '1918-01-27 07:11:00']
26+
})
27+
for col in main_sleep_df.columns:
28+
main_sleep_df[col] = pd.to_datetime(main_sleep_df[col])
29+
30+
diary_nights_df = pd.DataFrame({
31+
'START': ['1918-01-24 23:00:00', '1918-01-25 22:00:00', '1918-01-27 00:00:00'],
32+
'END': ['1918-01-25 07:00:00', '1918-01-26 07:30:00', '1918-01-27 07:30:00']
33+
})
34+
for col in diary_nights_df.columns:
35+
diary_nights_df[col] = pd.to_datetime(diary_nights_df[col])
36+
37+
sol = {}
38+
39+
for idx, row in diary_nights_df.iterrows():
40+
date = row['START']
41+
matches = main_sleep_df[main_sleep_df['start_time'].dt.date == date.date()]
42+
if not matches.empty:
43+
sleep_onset = matches.iloc[0]['start_time']
44+
latency = sleep_onset - row['START']
45+
sol[date.date()] = latency
46+
sol = pd.Series(sol)
47+
return pd.Series(sol), np.mean(sol)
48+
49+
50+
if __name__ == '__main__':
51+
main()

0 commit comments

Comments
 (0)