Finding Optimal Bedtime Windows using Oura Data

Thu, Dec 17, 2020 3-minute read 484 words

In this post, I demonstrate how Oura ring data can be used to identify the optimal bed time window that results in a higher quality sleep.

Some sleep researchs studies suggest that going to bed and waking up at certain times can result in a more optimal sleep. These individual bedtimes windows depend on person’s chronotype and circadian rhythms. To find my personal optimal lights-off/lights-on time, I turn to my Oura data. This analysis is based on data collected during ~650 nights in 2019 and 2020.

sleep stages in Oura ring data

More Bedtime =/= Better Sleep

First, let’s see if simply staying longer in bed leads to a higher sleep quality. Thankfully, in addition to time in bed (“duration”), my Oura ring tracks all stages of sleep: light sleep, REM and deep. Similar to total sleep efficiency, I convert the time in each of the sleep stages into proportions of the total sleep time. I am also primarily interested in restorative stages (REM and deep):

sleepbedtime<-ouradata %>% select(duration,light,rem,deep) %>%  
    drop_na %>% 
    mutate(totsleep=(light+rem+deep)) %>% 
    mutate(vars(light,rem,deep),list(~round(100*(./totsleep),1))) %>% 
    mutate(restsleep=rem+deep)

The correlations partially confirms our assumptions: more time in bed does not necessarily result in a better sleep - at least not all stages.

bedtime vs sleep quality correlations

Finding Optimal Bedtime Windows

Original Oura data has bedtime_start and bedtime_end in %Y-%m-%dT%H:%M:%S format so I have to go through some manipulations to convert those into time slots:

mutate(bedtime_end=substr(bedtime_end,1,19)) %>% 
  mutate(bedstart=as.numeric(format(strptime(bedtime_start,"%Y-%m-%dT%H:%M:%S"),'%H.%M'))) %>% 
  mutate(bedend=as.numeric(format(strptime(bedtime_end,"%Y-%m-%dT%H:%M:%S"),'%H.%M'))) %>% 
  mutate(start = 1*(bedstart>12 & bedstart<22.3) + 2*(bedstart>=22.3 & bedstart <23)+ 
                  3*(bedstart >=23) + 3*(bedstart<12)) %>% 
  mutate(bedtime_start=c("<10:30PM","10:30-11:00PM",">11:00PM")[start]) %>% 
  mutate(bedend=as.numeric(format(strptime(bedtime_end,"%Y-%m-%dT%H:%M:%S"),'%H.%M'))) %>% 
  mutate(end = 1*(bedend<6.3) + 2*(bedend>=6.3 & bedend <7)+ 
           3*(bedend >=7 & bedend <7.3) + 4*(bedend>=7.3)) %>%  
  mutate(bedtime_end=c("<6:30AM","6:30-7:00AM","7:00-7:30AM",">7:30AM")[end])

I had to play with various breakpoints before I arrived at the more or less decent version with enough records within each bucket:

distribution of bedtime start and end breakpoints

I then used heatmap to visualize the mean differences in restorative sleep across timeslot combinations. Please note that I am not running any statistical significance tests. But I did include the sample sizes (in parentheses).

restorative sleep across bedtime windows

According to this heatmap, the best way to get the most out of my sleep is to go to bed before 10:30 pm and get up between 6:30 and 7:00 am. Interestingly, I can get almost the same proportion of restorative sleep if I go to bed after 11 pm and wake up between 6:30 and 7:00 am. What is also interesting is that all optimal values are clustered within the 6:30AM-7:30Am timeslot, and thus are driven mostly by the time of waking up, and not the time of going to bed.

The two potential issues I see with this analysis are:

  • my choice of the metric (efficiency of REM + Deep sleep) may not necessarily be ideal or even correct
  • the sample sizes are too small to make a definitive conclusion; I may need to start going to bed earlier more often.

I will certainly have to replicate this analysis once I get at least six months more of data.