Visualizing An Infant's Schedule

A newborn may only eat, sleep, and poop, but they sure do it a lot! Here is a sample of two weeks of an an infant's schedule visualized with matplotlib.

A while ago I came across a radial sleep schedule plot and thought it was a creative way to visualize this type of data.

Since my wife and I have been tracking data for use with the doctor already using the Baby Tracker app, and since it has a convenient exoprt to csv feature, I decided to make a similar chart.

To export the data from the Baby Tracker app go to Settings > Export Data to CSV.

This will generate a csv.zip file that you can email to yourself. Unzipping the file gives one csv for each category: sleep, nursing, diaper, etc.

Using sleep as an example, we start by loading the data into a Pandas DataFrame:

In [1]:
import pandas as pd
sleep = pd.read_csv('Baby1_sleep.csv',parse_dates=[1])
sleep.head()
Out[1]:
Baby Time Duration(minutes) Note
0 Baby1 2018-08-01 00:38:00 92 NaN
1 Baby1 2018-08-01 03:11:00 108 NaN
2 Baby1 2018-08-01 05:44:00 131 NaN
3 Baby1 2018-08-01 09:05:00 83 NaN
4 Baby1 2018-08-01 11:16:00 109 NaN

This gives us the start time and duration of each period of sleep. Our matplotlib chart is a bar chart on a polar projection. Therefore, we need to define the center, thickness, width, bottom of each bar:

  • center is the midpoint of the nap, or (end time - start time) / 2, which we then convert to polar coordinates and offset so midnight is at the bottom
  • thickness is a constant, we use 1
  • width is the duration of the nap converted to polar coordinates and rotated
  • bottom is the radial position of the bar, which corresponds to the date since each "ring" on the chart is a day

Putting this together with some hour labels, we get a simple chart of baby's sleep pattern over two weeks:

Nursing time has a similar duration, and different colored bars can be added in the same manner. Other data such as diapers are single events without a duration:

In [2]:
diaper = pd.read_csv('Baby1_diaper.csv',parse_dates=[1])
diaper.head()
Out[2]:
Baby Time Status Note
0 Bab1 2018-08-01 02:22:00 Mixed NaN
1 Bab1 2018-08-01 05:11:00 Mixed NaN
2 Bab1 2018-08-01 08:05:00 Mixed NaN
3 Bab1 2018-08-01 10:40:00 Mixed NaN
4 Bab1 2018-08-01 13:25:00 Mixed NaN

For these, we plot a simple point instead of a bar. Adding a nice legend and a some story time, we get the final chart:

Here is the full code (or download here):

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
from matplotlib.lines import Line2D

def data_sleep(path):
    # read data
    sleep = pd.read_csv(path,
                        parse_dates=[1])
    sleep['DateTime'] = sleep['Time']
    sleep['Date'] = sleep['DateTime'].dt.date
    sleep['Time'] = sleep['DateTime'].dt.time

    # get bottom
    sleep['bottom'] = (sleep['Date']-min(sleep['Date'])).astype(str)
    sleep['bottom'] = (sleep['bottom'].str.split(' ').str.get(0)).astype(int)

    # get minutes
    sleep['minutes'] = sleep['Time'].astype(str).str.split(':').apply(lambda x: int(x[0]) * 60 + int(x[1]))

    return sleep


def data_nurse(path):
    # read data
    nurse = pd.read_csv(path,
                        parse_dates=[1])
    nurse['DateTime'] = nurse['Time']
    nurse['Date'] = nurse['DateTime'].dt.date
    nurse['Time'] = nurse['DateTime'].dt.time

    # get duration
    nurse['Total Duration'] = nurse['Left duration'] + nurse['Right duration']

    # get bottom
    nurse['bottom'] = (nurse['Date']-min(nurse['Date'])).astype(str)
    nurse['bottom'] = (nurse['bottom'].str.split(' ').str.get(0)).astype(int)

    # get minutes
    nurse['minutes'] = nurse['Time'].astype(str).str.split(':').apply(lambda x: int(x[0]) * 60 + int(x[1]))

    return nurse


def data_diaper(path):
    # read data
    diaper = pd.read_csv(path,
                        parse_dates=[1])
    diaper['DateTime'] = diaper['Time']
    diaper['Date'] = diaper['DateTime'].dt.date
    diaper['Time'] = diaper['DateTime'].dt.time

    # get bottom
    diaper['bottom'] = (diaper['Date']-min(diaper['Date'])).astype(str)
    diaper['bottom'] = (diaper['bottom'].str.split(' ').str.get(0)).astype(int)

    # get minutes
    diaper['minutes'] = diaper['Time'].astype(str).str.split(':').apply(lambda x: int(x[0]) * 60 + int(x[1]))

    return diaper


def data_story(path):
    # read data
    story = pd.read_csv(path,
                        parse_dates=[1])
    story = story[story['Other activity']=='Story Time']
    story['DateTime'] = story['Time']
    story['Date'] = story['DateTime'].dt.date
    story['Time'] = story['DateTime'].dt.time

    # get bottom
    story['bottom'] = (story['Date']-min(story['Date'])).astype(str)
    story['bottom'] = (story['bottom'].str.split(' ').str.get(0)).astype(int)

    # get minutes
    story['minutes'] = story['Time'].astype(str).str.split(':').apply(lambda x: int(x[0]) * 60 + int(x[1]))

    return story


def plot_sleep_radial(sleep):
    ax = plt.subplot(111, projection='polar')
    ax.set_facecolor('#F8B55A') # awake time background

    # turn axes off
    ax.axes.get_xaxis().set_visible(False)
    ax.axes.get_yaxis().set_visible(False)

    # sleep time
    for row in range(sleep.shape[0]):
        duration = sleep.loc[sleep.index[row],'Duration(minutes)']
        minutes  = sleep.loc[sleep.index[row],'minutes']
        bottom   = sleep.loc[sleep.index[row],'bottom']
        width = duration / 60 / 24 * 2 * np.pi
        center = ( -(minutes+duration/2) / 60 / 24 * 2 * np.pi) - np.pi/2
        bars = ax.bar(center, 1, width=width, bottom=bottom, color='#225282')

    # hour marks
    x = np.linspace(0,np.pi*2,num=25)
    hr = ['6 pm','5 pm','4 pm','3 pm','2 pm','1 pm','12 pm',
          '11 am','10 am','9 am','8 am','7 am','6 am',
          '5 am','4 am','3 am','2 am','1 am','12 am',
          '11 pm','10 pm','9 pm','8 pm','7 pm','']
    for i in range(len(x)):
        plt.plot([0,x[i]],[0,bottom+1],'k:',lw=.5,alpha=.5)
        plt.text(x[i],bottom+2.5,hr[i],ha='center',va='center', color='grey', 
            fontsize=8, family='monospace')

    # annotation
    ax.annotate('Data collected with Baby Tracker app (v3.09)', xy=(0, 0),  
        xycoords='figure fraction', xytext=(0.0, -0.1), textcoords='axes fraction',
        horizontalalignment='left', verticalalignment='center', fontsize=6, color='grey')

    # legend
    legend_elements = [Line2D([0], [0], color='#F8B55A', lw=6, label='Awake'),
                       Line2D([0], [0], color='#225282', lw=6, label='Sleeping')]                       
    ax.legend(handles=legend_elements, loc='upper left', bbox_to_anchor=(.85, .06), fontsize=6)


    plt.title('Baby\'s Sleep Schedule', va='bottom')
    plt.ylim(ymax=bottom+1)
    plt.savefig('plot_sleep_radial.png', bbox_inches='tight', dpi=300)


def plot_all_radial(sleep, nurse, diaper, story):
    ax = plt.subplot(111, projection='polar')
    ax.set_facecolor('#F8B55A') # awake time background

    # turn axes off
    ax.axes.get_xaxis().set_visible(False)
    ax.axes.get_yaxis().set_visible(False)

    # sleep time
    for row in range(sleep.shape[0]):
        duration = sleep.loc[sleep.index[row],'Duration(minutes)']
        minutes  = sleep.loc[sleep.index[row],'minutes']
        bottom   = sleep.loc[sleep.index[row],'bottom']
        width = duration / 60 / 24 * 2 * np.pi
        center = ( -(minutes+duration/2) / 60 / 24 * 2 * np.pi) - np.pi/2
        bars = ax.bar(center, 1, width=width, bottom=bottom, color='#225282')

    # nurse time
    for row in range(nurse.shape[0]):
        duration = nurse.loc[nurse.index[row],'Total Duration']
        minutes  = nurse.loc[nurse.index[row],'minutes']
        bottom   = nurse.loc[nurse.index[row],'bottom']
        width = duration / 60 / 24 * 2 * np.pi
        center = ( -(minutes+duration/2) / 60 / 24 * 2 * np.pi) - np.pi/2
        bars = ax.bar(center, 1, width=width, bottom=bottom, color='#f85a9d')

    # dirty diapers
    for row in range(diaper.shape[0]):
        bottom   = diaper.loc[diaper.index[row],'bottom']
        minutes  = diaper.loc[diaper.index[row],'minutes']
        center = ( -minutes / 60 / 24 * 2 * np.pi) - np.pi/2
        plt.plot(center, bottom+.5, marker='o', markersize=3, color="#825222", 
            markeredgecolor='white', markeredgewidth=.2)

    # story time
    for row in range(story.shape[0]):
        bottom   = story.loc[story.index[row],'bottom']
        minutes  = story.loc[story.index[row],'minutes']
        center = ( -minutes / 60 / 24 * 2 * np.pi) - np.pi/2
        plt.plot(center, bottom+.5, marker='o', markersize=3, color="#5aecf8", 
            markeredgecolor='white', markeredgewidth=.2)

    # hour marks
    x = np.linspace(0,np.pi*2,num=25)
    hr = ['6 pm','5 pm','4 pm','3 pm','2 pm','1 pm','12 pm',
          '11 am','10 am','9 am','8 am','7 am','6 am',
          '5 am','4 am','3 am','2 am','1 am','12 am',
          '11 pm','10 pm','9 pm','8 pm','7 pm','']
    for i in range(len(x)):
        plt.plot([0,x[i]],[0,bottom+1],'k:',lw=.5,alpha=.5)
        plt.text(x[i],bottom+2.5,hr[i],ha='center',va='center', color='grey', 
            fontsize=8, family='monospace')

    # annotation
    ax.annotate('Data collected with Baby Tracker app (v3.09)', xy=(0, 0),
        xycoords='figure fraction', xytext=(0.0, -0.1), textcoords='axes fraction',
        horizontalalignment='left', verticalalignment='center', fontsize=6, color='grey')

    # legend
    legend_elements = [Line2D([0], [0], color='#F8B55A', lw=6, label='Awake'),
                       Line2D([0], [0], color='#225282', lw=6, label='Sleeping'),
                       Line2D([0], [0], color='#f85a9d', lw=6, label='Nursing'),
                       Line2D([0], [0], marker='o', color='#825222', label='Diaper',
                              markeredgecolor='w', markersize=7),
                       Line2D([0], [0], marker='o', color='#5aecf8', label='Story Time',
                              markeredgecolor='w', markersize=7)]
    ax.legend(handles=legend_elements, loc='upper left', bbox_to_anchor=(.85, .06), fontsize=6)

    plt.title('Baby\'s Schedule', va='bottom')
    plt.ylim(ymax=bottom+1)
    plt.savefig('plot_all_radial.png', bbox_inches='tight', dpi=300)


sleep = data_sleep('Baby1_sleep.csv')
nurse = data_nurse('Baby1_nursing.csv')
diaper = data_diaper('Baby1_diaper.csv')
story = data_story('Baby1_other_activity.csv')

plot_sleep_radial(sleep)
plot_all_radial(sleep, nurse, diaper, story)

Library versions:

matplotlib  2.2.2
pandas      0.22.0
Python      3.6.3

© 2005 Matthew Kudija | Source