Baby sleep: the first six months

In the haze of sleep deprivation, it can be difficult to remember when the baby last ate or how long ago you changed her. A friend recommended the Feed Baby app to track diaper changes, feeds, sleep, weight, and so on, and we immediately traded in the pen + paper tracker provided by the hospital for a digital tracker.

I assumed I’d be able to use the meticulously tracked data to better understand my baby, and maybe even figure out how to make her sleep longer. [Narrator: She did not.] We learned a lot of lessons along the way, but ultimately, better sleep just took time. Here's a summary of our baby's sleep development during her first six months:

The first few weeks were all over the place, with just about as much day sleep as night sleep. Being first time parents, it took us a little while to figure out how to get her to fall asleep and stay asleep. Luckily(?) being awake at all hours affords you plenty of time to Google anything and everything. We started a bedtime routine, made some changes to her nursery - blackout curtains, a noise machine - and started paying attention to wake windows. Eventually, we rented a Snoo smart bassinet.

Those changes started paying off around week 11 when a pattern (OMG yes finally) of naps and night sleep started to emerge. Unfortunately, that pattern was five 45 minute naps with about 90 minutes of wake time in between. Considering the often long effort to get her to fall asleep (rocking, shushing, rocking, sweating), holding her for the nap, and then a 45 minute nursing session (slow eater), you might get 20 minutes of play/bathroom break/walk-around-the-block time in each nap cycle.  After a while, we mastered the transfer to the bassinet and felt like new humans who could do anything!! for those short blocks of freedom. Now, I miss holding my sleeping bug in my arms.  

At the same time, night sleep continued to improve. There was a short regression around eight weeks (barely remember this) and a long one starting around 18 weeks (definitely remember this).

The four month sleep regression lasted for about a month, and. It. Hurt. We went from what felt like slow but steady progress to sleeping like a three week old. She was up 4-6 times a night, often for an hour or longer each time. She was getting increasingly difficult to get back to sleep. It didn't help that she was also fighting what would end up being a six-week (!) runny nose from daycare.

When she was a newborn, I'd nurse her at most wake-ups because I assumed she was hungry (and she almost always was). Now, that wasn't feasible - nursing didn't quite put her back to sleep anymore, I knew she could go longer than three hours past bedtime without eating, and I didn't want her to reverse cycle and start getting all of her calories overnight. We started to kick around the idea of sleep training (essentially the task of teaching your bub to fall asleep on their own) but it felt too soon for her. Plus, we were still using the Snoo and a swaddle; so, we decided to retire both to clear the way. We dropped the swaddle with little issue (so surprising) and moved to the crib with some issues (not surprising).

She started to show some signs of being able to fall asleep on her own. When she was 21 weeks old, she fell asleep in the car for the first time (go ahead and let that one sink in).

We implemented a Ferber-style wait and check sleep training approach around week 24. After a few tough nights (I think I may have cried more into my pint of Ben & Jerry's than she did) she is now falling asleep babbling to herself and generally going from 8 p.m.-6 a.m. with a few beeps and boops that do not require our intervention. Naps are still unpredictable and require rocking/sneakily transferring to the crib, but we still feel like WE HAVE MADE IT. (Parents of older babies, don't tell me what comes next, please please please, I am purposely blocking out all mentions of the eight-month sleep regression).

Despite these leaps and bounds, her total hours of sleep has stayed pretty constant.

In general, 14-17 hours per day is recommended/considered normal for newborns, but ours averaged just 12. I guess she's just on the left side of the baby sleep bell curve because I am prettttttty sure it's not our lack of trying.

***

The accompanying R code is below and on Github.

Feed Baby

The app also has a feature that lets you download the data as several .csv files (one for sleep, one for feeds, etc). Here’s a look at the sleep.csv export.

str(feedbaby_raw)
## 'data.frame':    1113 obs. of  5 variables:
##  $ id                            : int  2 1 3 4 5 6 7 8 9 10 ...
##  $ Start.Time                    : chr  "11:20PM 01-24-2020" "1:40AM 01-25-2020" "8:30AM 01-25-2020" "3:25PM 01-25-2020" ...
##  $ End.Time                      : chr  "12:08AM 01-25-2020" "7:20AM 01-25-2020" "12:00PM 01-25-2020" "6:42PM 01-25-2020" ...
##  $ Notes                         : chr  "" "" "" "" ...
##  $ Approximate.Duration..Minutes.: int  48 340 210 197 45 132 101 77 39 223 ...

Preprocessing the data

First, I split sessions into individual days (e.g. sessions crossing over midnight would become one ending at 11:59 PM and a second one starting at 12:00 AM). For cleaner visualization, I then shifted the times to allow for a plot with the x axis beginning at 7 AM rather than 12 AM to more clearly delineate day and night sleeps.

date_shift_and_split <- function(df,
                                 start_time_numeric = 'start_time_numeric', 
                                 end_time_numeric = 'end_time_numeric',
                                 shifted_day_start = 7) {
  
  shifted_df <- df %>%
    mutate(start_time_shift = ifelse(!! sym(start_time_numeric) < shifted_day_start, !! sym(start_time_numeric) + 24, !! sym(start_time_numeric)),
           end_time_shift = ifelse(!! sym(end_time_numeric) < shifted_day_start, !! sym(end_time_numeric) + 24, !! sym(end_time_numeric)),
           start_date_shift = as.Date(ifelse(start_time_shift >= 24, start_date - lubridate::days(1), start_date), origin = '1970-01-01'),
           end_date_shift = as.Date(ifelse(end_time_shift >= 24, end_date - lubridate::days(1), end_date), origin = '1970-01-01'),
           split = ifelse(start_date_shift != end_date_shift, 'original', NA)
           )
  
  splits <- shifted_df %>%
    filter(split == 'original') %>%
    mutate(split = 'duplicate')

  out_df <- shifted_df %>%
    rbind(splits) %>%
    arrange(start_date_shift, start_time_shift, split) %>%
    mutate(end_time_shift = case_when(split == 'original' ~ (shifted_day_start + 24), 
                                      TRUE ~ end_time_shift),
           start_time_shift = case_when(split == 'duplicate' ~ shifted_day_start,
                                        TRUE ~ start_time_shift)
  )
  
  return(out_df)

}

session_type <- function(df, 
                         night_start = 18.75, 
                         night_end = 7, 
                         start_time = 'start_time_shift',
                         end_time = 'end_time_shift'){
  
  df %>%
    mutate(type = ifelse(!! sym(start_time) >= night_start | !! sym(end_time) < night_end,
                         'night', 
                         'day'))
  
}

baby_age <- function(df, 
                     start_date = 'start_date_shift',
                     birth_date = as.Date('2020-01-21')){
  
  df %>%
    mutate(days_old = as.numeric(difftime(!! sym(start_date), birth_date)), 
           weeks_old = days_old/7)
}

preprocess_sleep <- function(df){
  
  df <- date_shift_and_split(df)
  df <- session_type(df)
  df <- baby_age(df)
  
  return(df)
  
}
feedbaby_sleep <- feedbaby_raw %>%
  mutate(start_datetime = parse_date_time(Start.Time, '%I:%M%p %m-%d-%Y'),
         end_datetime = parse_date_time(End.Time, '%I:%M%p %m-%d-%Y'),
         start_date = mdy(format(start_datetime, '%m-%d-%y')),
         start_time_numeric = hour(start_datetime) + minute(start_datetime)/60,
         end_date = mdy(format(end_datetime, '%m-%d-%y')),
         end_time_numeric = hour(end_datetime) + minute(end_datetime)/60,
         location = 'all sleeps'
         ) %>%
  select(c('start_datetime', 'end_datetime', 'start_date', 'end_date', 'start_time_numeric', 'end_time_numeric', 'location'))

feedbaby_sleep_processed <- preprocess_sleep(feedbaby_sleep)

str(feedbaby_sleep_processed)
## 'data.frame':    1158 obs. of  15 variables:
##  $ start_datetime    : POSIXct, format: "2020-01-24 23:20:00" "2020-01-25 01:40:00" ...
##  $ end_datetime      : POSIXct, format: "2020-01-25 00:08:00" "2020-01-25 07:20:00" ...
##  $ start_date        : Date, format: "2020-01-24" "2020-01-25" ...
##  $ end_date          : Date, format: "2020-01-25" "2020-01-25" ...
##  $ start_time_numeric: num  23.33 1.67 1.67 8.5 15.42 ...
##  $ end_time_numeric  : num  0.133 7.333 7.333 12 18.7 ...
##  $ location          : chr  "all sleeps" "all sleeps" "all sleeps" "all sleeps" ...
##  $ start_time_shift  : num  23.3 7 25.7 8.5 15.4 ...
##  $ end_time_shift    : num  24.13 7.33 31 12 18.7 ...
##  $ start_date_shift  : Date, format: "2020-01-24" "2020-01-24" ...
##  $ end_date_shift    : Date, format: "2020-01-24" "2020-01-25" ...
##  $ split             : chr  NA "duplicate" "original" NA ...
##  $ type              : chr  "night" "day" "night" "day" ...
##  $ days_old          : num  3 3 3 4 4 4 4 4 4 4 ...
##  $ weeks_old         : num  0.429 0.429 0.429 0.571 0.571 ...
feedbaby_sleep_processed %>%
  ggplot() + 
  geom_segment(aes(x = start_time_shift, 
                   xend = end_time_shift, 
                   y = weeks_old, 
                   yend = weeks_old,
                   color = type), 
               size = 2) +
  geom_rect(aes(xmin = 7, xmax = 31, ymin = 0, ymax = 3/7), fill = 'grey60') +
  scale_x_continuous(breaks = c(7,13,19,25,31), 
                     labels = c('7AM', '1PM', '7PM', '1AM', '7AM')) +
  scale_y_continuous(breaks = seq(from = 2, to = 26, by = 2)) +
  coord_flip() +
  labs(title = 'Newborn sleep', 
       subtitle = 'Tracked using the Feed Baby app',
       x = 'Time', 
       y = 'Weeks old') +
  scale_color_manual(values = c('#5DA9E9','#490B32')) +
  theme(panel.grid.major.x = element_line(color="grey80"),
        panel.grid.major.y = element_blank(),
        legend.position = 'top') 
feedbaby_sleep_processed %>%
  mutate(duration_hours = end_time_shift - start_time_shift) %>% 
  group_by(weeks_old, type) %>%
  summarise(total_hours = sum(duration_hours)) %>% 
  ungroup() %>%
  ggplot(aes(x = weeks_old, y = total_hours)) + 
  geom_col(aes(fill = type)) +
  scale_x_continuous(breaks = seq(from = 2, to = 26, by = 2)) +
  scale_y_continuous(breaks = seq(from = 2, to = 16, by = 2)) +
  labs(title = 'Total hours of sleep',
       x = 'Weeks old',
       y = 'Hours') +
  scale_fill_manual(values = c('#5DA9E9','#490B32'))
feedbaby_sleep_processed %>%
  mutate(duration_hours = end_time_shift - start_time_shift) %>% 
  group_by(start_date_shift, weeks_old, type) %>%
  summarise(longest_stretch = max(duration_hours)) %>% 
  ggplot(aes(x = weeks_old, y = longest_stretch)) + 
  geom_line(aes(color = type)) +
  geom_point(aes(color = type)) +
    annotate('rect', xmin = (8 + 1/7), xmax = (10 + 3/7), ymin = 0, ymax = 12, alpha = 0.05, fill = '#9A031E') +
  annotate('rect', xmin = (18 + 1/7), xmax = (21 + 6/7), ymin = 0, ymax = 12, alpha = 0.05, fill = '#9A031E') +
  annotate('text', x = 9.285, y = 11.5, label = '8 week sleep regression') +
  annotate('text', x = 20, y = 11.5, label = '4 month sleep regression') +
  annotate('text', x = (24 + 1/7), y = 2.5, label = 'Transition to crib') +
  scale_x_continuous(breaks = seq(from = 2, to = 26, by = 2)) +
  scale_y_continuous(breaks = seq(from = 0, to = 12, by = 2), limits = c(0,12)) + 
  labs(title = 'Longest stretch of sleep',
       x = 'Weeks old',
       y = 'Hours') +
  scale_color_manual(values = c('#5DA9E9','#490B32'))
Show Comments