Analysis of E. coli growth data¶

(c) 2024 Justin Bois. This work is licensed under a Creative Commons Attribution License CC-BY 4.0. All code contained herein is licensed under an MIT license.

This document was prepared at Caltech with support financial support from the Donna and Benjamin M. Rosen Bioengineering Center.

No description has been provided for this image

This tutorial was generated from an Jupyter notebook. You can download the notebook here.


In [1]:
import pandas as pd
import numpy as np

import scipy.optimize

import colorcet
import iqplot

import bokeh.io
import bokeh.plotting
bokeh.io.output_notebook()
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Loading BokehJS ...

In this tutorial, we walk through analysis of growth data as acquired in our bulk assay using the plate reader. The analysis presents three main challenges that we have not covered in previous tutorials.

  1. We need to wrangle the data exported from the plate reader in a text file to enable easy access and analysis.
  2. We need to make plots, some on semilogarithmic scale, to visualize the results.
  3. We need to consider portions of the data set in performing regressions, so we need to select and use only parts of the data set.

Data wrangling¶

Our first step is taking the plate reader data and converting it to an easy format to interpret. As a sample data set, we will use data obtained by the TAs in one of their experiments while developing this module. The data set is in the file 20240422_ecoli_growth_trial_1.txt, which is available here. The first 57 lines of this file are shown below.



Software Version	3.10.06



Experiment File Path:	C:\Users\User\Desktop\20240422_ecoli_growth_trial_1.xpt
Protocol File Path:	C:\Users\Public\Documents\Protocols\od600.prt



Plate Number	Plate 1
Date	4/22/2024
Time	9:47:23 AM
Reader Type:	Synergy H1
Reader Serial Number:	2107069
Reading Type	Reader

Procedure Details

Plate Type	96 WELL PLATE (Use plate lid)
Eject plate on completion	
Set Temperature	Setpoint 37°C
Read	Absorbance Endpoint
    Full Plate
    Wavelengths:  600
    Read Speed: Normal,  Delay: 100 msec,  Measurements/Data Point: 8
Start Kinetic	Runtime 24:00:00 (HH:MM:SS), Interval 0:06:00, 241 Reads
    Shake	Linear: Continuous
    Frequency: 1096 cpm (1 mm)
    Read	Absorbance Endpoint
    Full Plate
    Wavelengths:  600
    Read Speed: Normal,  Delay: 100 msec,  Measurements/Data Point: 8
End Kinetic	



Actual Temperature:	23.9

Results
    1	2	3	4	5	6	7	8	9	10	11	12
A	0.086	0.095	0.095	0.093	0.095	0.096	0.090	0.087	0.085	0.084	0.085	0.083	Read 1:600
B	0.091	0.146	0.131	0.121	0.123	0.105	0.110	0.107	0.085	0.084	0.085	0.083	Read 1:600
C	0.092	0.136	0.127	0.105	0.114	0.125	0.133	0.106	0.086	0.083	0.088	0.084	Read 1:600
D	0.090	0.098	0.104	0.103	0.101	0.132	0.143	0.134	0.091	0.090	0.094	0.086	Read 1:600
E	0.085	0.086	0.085	0.087	0.085	0.122	0.109	0.105	0.085	0.084	0.085	0.084	Read 1:600
F	0.085	0.085	0.085	0.084	0.084	0.127	0.113	0.107	0.084	0.086	0.084	0.083	Read 1:600
G	0.084	0.084	0.084	0.093	0.086	0.122	0.104	0.093	0.088	0.086	0.084	0.085	Read 1:600
H	0.084	0.085	0.084	0.085	0.085	0.085	0.085	0.085	0.085	0.088	0.083	0.084	Read 1:600

Read 2:600

Time	T° Read 2:600	A1	A2	A3	A4	A5	A6	A7	A8	A9	A10	A11	A12	B1	B2	B3	B4	B5	B6	B7	B8	B9	B10	B11	B12	C1	C2	C3	C4	C5	C6	C7	C8	C9	C10	C11	C12	D1	D2	D3	D4	D5	D6	D7	D8	D9	D10	D11	D12	E1	E2	E3	E4	E5	E6	E7	E8	E9	E10	E11	E12	F1	F2	F3	F4	F5	F6	F7	F8	F9	F10	F11	F12	G1	G2	G3	G4	G5	G6	G7	G8	G9	G10	G11	G12	H1	H2	H3	H4	H5	H6	H7	H8	H9	H10	H11	H12
0:05:10	29.8	0.083	0.084	0.085	0.085	0.084	0.086	0.087	0.085	0.085	0.084	0.091	0.084	0.084	0.106	0.099	0.106	0.100	0.102	0.101	0.097	0.085	0.084	0.085	0.083	0.083	0.106	0.108	0.106	0.107	0.105	0.113	0.103	0.085	0.083	0.087	0.084	0.084	0.092	0.100	0.097	0.091	0.134	0.126	0.104	0.091	0.090	0.094	0.086	0.084	0.086	0.085	0.087	0.084	0.106	0.104	0.099	0.085	0.084	0.085	0.084	0.086	0.084	0.085	0.084	0.084	0.106	0.102	0.103	0.084	0.086	0.084	0.083	0.084	0.084	0.084	0.093	0.085	0.104	0.104	0.101	0.088	0.086	0.084	0.085	0.084	0.085	0.084	0.085	0.085	0.085	0.085	0.085	0.085	0.088	0.083	0.084
0:11:10	33.9	0.083	0.084	0.085	0.085	0.084	0.086	0.087	0.085	0.085	0.084	0.091	0.084	0.084	0.104	0.100	0.105	0.100	0.102	0.100	0.101	0.085	0.084	0.084	0.083	0.083	0.103	0.103	0.103	0.103	0.102	0.106	0.101	0.085	0.083	0.087	0.084	0.084	0.092	0.100	0.097	0.091	0.128	0.123	0.105	0.091	0.091	0.094	0.086	0.084	0.086	0.085	0.087	0.084	0.101	0.101	0.100	0.085	0.084	0.085	0.084	0.086	0.084	0.085	0.084	0.084	0.103	0.107	0.102	0.084	0.085	0.084	0.083	0.084	0.084	0.084	0.093	0.085	0.101	0.104	0.100	0.088	0.086	0.084	0.085	0.084	0.085	0.084	0.085	0.085	0.085	0.085	0.085	0.085	0.088	0.083	0.084

Evidently, the plate reader returns a text file with some useful, but unformatted metadata about the measurement, followed by the data. Following the metadata is the first set of absorbance measurements in a table. Following that is the text Read 2:600, which contains a tab-delimited table of absorbance measurements.

Pulling the first time point out is not important for our measurements (the plate reader has not come to temperature anyway), so we will neglect it. Therefore, to pull the data set out of the file, we need to load in the last table. The information we need to do this are the file name, the row number (with zero-based indexing) of the header for the table (containing the well labels), and the last row of the file containing data. WE can get all of this information by opening the file in a text editor. For this particular data set, these parameters are defined below.

In [2]:
# Useful info from file from plate reader
fname = "20240422_ecoli_growth_trial_1.txt"
header_row = 53
last_row = 294

We can now write a function to pull the data out and store them in a data frame. Importantly, we will add a column labeled 'time (s)' which is the amount of time, in seconds, since the first measurement we consider, and a column labeled 'time (hr)', which is the same time, but in units of hours.

In [3]:
def read_plate_reader(fname, header_row, last_row):
    """Read in time series data from Bi 1x plate reader.

    Parameters
    ----------
    fname : str
        Path to file outputted from plate reader.
    header_row : int
        Number of row containing header column (zero-indexed)
    last_row : int
        Last row of the file containing the time series data (zero-indexed)

    Returns
    -------
    output : DataFrame
        Pandas DataFrame containing time series data.
    """
    # Find out how many rows in input file
    with open(fname, "rb") as fp:
        n_rows = len(fp.readlines())
    
    # How many lines in footer to skip
    skipfooter = n_rows - last_row - 1

    # Read in data frame
    df = pd.read_csv(
        fname,
        sep="\t",
        skiprows=header_row,
        skipfooter=skipfooter,
        engine="python",
        encoding='ISO-8859-1',
    )

    # Rename temperature column
    df = df.rename(columns={df.columns[df.columns.str.contains('T°')][0]: 'temperature (deg C)'})

    # Parse the time column
    df['time (s)'] = pd.to_timedelta(df['Time']).dt.total_seconds()

    # Set start time to zero
    df['time (s)'] -= df['time (s)'].min()

    # Time in units of hours
    df['time (hr)'] = df['time (s)'] / 3600

    return df

Let's use this function to load in the data and take a look.

In [4]:
df = read_plate_reader(fname, header_row, last_row)

df
Out[4]:
Time temperature (deg C) A1 A2 A3 A4 A5 A6 A7 A8 ... H5 H6 H7 H8 H9 H10 H11 H12 time (s) time (hr)
0 0:05:10 29.8 0.083 0.084 0.085 0.085 0.084 0.086 0.087 0.085 ... 0.085 0.085 0.085 0.085 0.085 0.088 0.083 0.084 0.0 0.0
1 0:11:10 33.9 0.083 0.084 0.085 0.085 0.084 0.086 0.087 0.085 ... 0.085 0.085 0.085 0.085 0.085 0.088 0.083 0.084 360.0 0.1
2 0:17:10 36.9 0.083 0.084 0.085 0.085 0.084 0.086 0.086 0.085 ... 0.085 0.085 0.085 0.085 0.085 0.088 0.083 0.084 720.0 0.2
3 0:23:10 37.0 0.083 0.084 0.085 0.085 0.083 0.086 0.086 0.085 ... 0.085 0.084 0.084 0.085 0.085 0.088 0.083 0.084 1080.0 0.3
4 0:29:10 37.0 0.082 0.084 0.085 0.085 0.083 0.086 0.088 0.085 ... 0.085 0.084 0.085 0.085 0.085 0.088 0.083 0.084 1440.0 0.4
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
236 23:41:10 37.0 0.082 0.083 0.084 0.085 0.083 0.086 0.087 0.084 ... 0.084 0.084 0.084 0.086 0.085 0.088 0.082 0.084 84960.0 23.6
237 23:47:10 37.0 0.082 0.083 0.084 0.085 0.083 0.086 0.086 0.084 ... 0.085 0.084 0.084 0.086 0.085 0.088 0.083 0.084 85320.0 23.7
238 23:53:10 37.0 0.082 0.083 0.084 0.085 0.083 0.086 0.084 0.084 ... 0.085 0.084 0.084 0.086 0.085 0.088 0.083 0.084 85680.0 23.8
239 23:59:10 37.0 0.082 0.083 0.084 0.085 0.083 0.086 0.084 0.084 ... 0.085 0.084 0.084 0.086 0.085 0.088 0.083 0.084 86040.0 23.9
240 24:05:10 37.0 0.082 0.083 0.084 0.085 0.083 0.086 0.084 0.084 ... 0.085 0.084 0.084 0.086 0.085 0.088 0.083 0.084 86400.0 24.0

241 rows × 100 columns

We now conveniently have a data set with our results.

We are, however, missing some metadata. We want to know, e.g., what conditions were present in well C6, for example. We can create a dictionary for our samples. For this particular trial, the TAs did not use all of the wells. Most of the wells were blanks containing only water (which has the same absorbance as M9 minimal media).

In [5]:
well_metadata = dict(
    A1='blank',
    A2='blank',
    A3='blank',
    A4='blank',
    A5='blank',
    A6='blank',
    A7='blank',
    A8='blank',
    A9='blank',
    A10='blank',
    A11='blank',
    A12='blank',
    B1='blank',
    B2='glucose',
    B3='galactose',
    B4='xylose',
    B5='sorbitol',
    B6='galactose:glucose 1:1',
    B7='xylose:glucose 1:1',
    B8='sorbitol:glucose 1:1',
    B9='blank',
    B10='blank',
    B11='blank',
    B12='blank',
    C1='blank',
    C2='glucose',
    C3='galactose',
    C4='xylose',
    C5='sorbitol',
    C6='galactose:glucose 1:1',
    C7='xylose:glucose 1:1',
    C8='sorbitol:glucose 1:1',
    C9='blank',
    C10='blank',
    C11='blank',
    C12='blank',
    D1='blank',
    D2='blank',
    D3='blank',
    D4='blank',
    D5='blank',
    D6='galactose:glucose 1:4',
    D7='xylose:glucose 1:4',
    D8='sorbitol:glucose 1:4',
    D9='blank',
    D10='blank',
    D11='blank',
    D12='blank',
    E1='blank',
    E2='blank',
    E3='blank',
    E4='blank',
    E5='blank',
    E6='galactose:glucose 1:4',
    E7='xylose:glucose 1:4',
    E8='sorbitol:glucose 1:4',
    E9='blank',
    E10='blank',
    E11='blank',
    E12='blank',
    F1='blank',
    F2='blank',
    F3='blank',
    F4='blank',
    F5='blank',
    F6='galactose:glucose 4:1',
    F7='xylose:glucose 4:1',
    F8='sorbitol:glucose 4:1',
    F9='blank',
    F10='blank',
    F11='blank',
    F12='blank',
    G1='blank',
    G2='blank',
    G3='blank',
    G4='blank',
    G5='blank',
    G6='galactose:glucose 4:1',
    G7='xylose:glucose 4:1',
    G8='sorbitol:glucose 4:1',
    G9='blank',
    G10='blank',
    G11='blank',
    G12='blank',
    H1='blank',
    H2='blank',
    H3='blank',
    H4='blank',
    H5='blank',
    H6='blank',
    H7='blank',
    H8='blank',
    H9='blank',
    H10='blank',
    H11='blank',
    H12='blank',
)

It is convenient to invert the dictionary so that we can look at which wells correspond to a given condition (e.g., wells B2 and C2 have glucose).

In [6]:
def invert_dict(input_dict):
    """Invert a dictionary ignore None values."""
    # Create an empty dictionary to store the inverted mapping
    output_dict = {}

    # Iterate over items in the input dictionary
    for key, value in input_dict.items():
        if value is not None:
            if value in output_dict:
                output_dict[value].append(key)
            else:
                output_dict[value] = [key]

    return output_dict

Let's now make and check out our inverse dictionary.

In [7]:
conditions = invert_dict(well_metadata)

conditions
Out[7]:
{'blank': ['A1',
  'A2',
  'A3',
  'A4',
  'A5',
  'A6',
  'A7',
  'A8',
  'A9',
  'A10',
  'A11',
  'A12',
  'B1',
  'B9',
  'B10',
  'B11',
  'B12',
  'C1',
  'C9',
  'C10',
  'C11',
  'C12',
  'D1',
  'D2',
  'D3',
  'D4',
  'D5',
  'D9',
  'D10',
  'D11',
  'D12',
  'E1',
  'E2',
  'E3',
  'E4',
  'E5',
  'E9',
  'E10',
  'E11',
  'E12',
  'F1',
  'F2',
  'F3',
  'F4',
  'F5',
  'F9',
  'F10',
  'F11',
  'F12',
  'G1',
  'G2',
  'G3',
  'G4',
  'G5',
  'G9',
  'G10',
  'G11',
  'G12',
  'H1',
  'H2',
  'H3',
  'H4',
  'H5',
  'H6',
  'H7',
  'H8',
  'H9',
  'H10',
  'H11',
  'H12'],
 'glucose': ['B2', 'C2'],
 'galactose': ['B3', 'C3'],
 'xylose': ['B4', 'C4'],
 'sorbitol': ['B5', 'C5'],
 'galactose:glucose 1:1': ['B6', 'C6'],
 'xylose:glucose 1:1': ['B7', 'C7'],
 'sorbitol:glucose 1:1': ['B8', 'C8'],
 'galactose:glucose 1:4': ['D6', 'E6'],
 'xylose:glucose 1:4': ['D7', 'E7'],
 'sorbitol:glucose 1:4': ['D8', 'E8'],
 'galactose:glucose 4:1': ['F6', 'G6'],
 'xylose:glucose 4:1': ['F7', 'G7'],
 'sorbitol:glucose 4:1': ['F8', 'G8']}

These kind of dictionaries need to be hand-built, based on what you do as an experimenter. For convenience, below is code for making a dictionary for your experiment. You should update the wells in row H to reflect what you and your classmates decided to put in there.

well_metadata = dict(
    A1="LB blank",
    A2="LB blank",
    A3="LB blank",
    A4="LB",
    A5="LB",
    A6="LB",
    A7="M9 blank",
    A8="M9 blank",
    A9="M9 blank",
    A10="glucose",
    A11="glucose",
    A12="glucose",
    B1="xylose",
    B2="xylose",
    B3="xylose",
    B4="xylose:glucose 4:1",
    B5="xylose:glucose 4:1",
    B6="xylose:glucose 4:1",
    B7="xylose:glucose 2:1",
    B8="xylose:glucose 2:1",
    B9="xylose:glucose 2:1",
    B10="xylose:glucose 1:1",
    B11="xylose:glucose 1:1",
    B12="xylose:glucose 1:1",
    C1="sorbitol",
    C2="sorbitol",
    C3="sorbitol",
    C4="sorbitol:glucose 4:1",
    C5="sorbitol:glucose 4:1",
    C6="sorbitol:glucose 4:1",
    C7="sorbitol:glucose 2:1",
    C8="sorbitol:glucose 2:1",
    C9="sorbitol:glucose 2:1",
    C10="sorbitol:glucose 1:1",
    C11="sorbitol:glucose 1:1",
    C12="sorbitol:glucose 1:1",
    D1="galactose",
    D2="galactose",
    D3="galactose",
    D4="galactose:glucose 4:1",
    D5="galactose:glucose 4:1",
    D6="galactose:glucose 4:1",
    D7="galactose:glucose 2:1",
    D8="galactose:glucose 2:1",
    D9="galactose:glucose 2:1",
    D10="galactose:glucose 1:1",
    D11="galactose:glucose 1:1",
    D12="galactose:glucose 1:1",
    E1="rhamnose",
    E2="rhamnose",
    E3="rhamnose",
    E4="rhamnose:glucose 4:1",
    E5="rhamnose:glucose 4:1",
    E6="rhamnose:glucose 4:1",
    E7="rhamnose:glucose 2:1",
    E8="rhamnose:glucose 2:1",
    E9="rhamnose:glucose 2:1",
    E10="rhamnose:glucose 1:1",
    E11="rhamnose:glucose 1:1",
    E12="rhamnose:glucose 1:1",
    F1="ribose",
    F2="ribose",
    F3="ribose",
    F4="ribose:glucose 4:1",
    F5="ribose:glucose 4:1",
    F6="ribose:glucose 4:1",
    F7="ribose:glucose 2:1",
    F8="ribose:glucose 2:1",
    F9="ribose:glucose 2:1",
    F10="ribose:glucose 1:1",
    F11="ribose:glucose 1:1",
    F12="ribose:glucose 1:1",
    G1="lactose",
    G2="lactose",
    G3="lactose",
    G4="lactose:glucose 4:1",
    G5="lactose:glucose 4:1",
    G6="lactose:glucose 4:1",
    G7="lactose:glucose 2:1",
    G8="lactose:glucose 2:1",
    G9="lactose:glucose 2:1",
    G10="lactose:glucose 1:1",
    G11="lactose:glucose 1:1",
    G12="lactose:glucose 1:1",
    H1="student choice 1",
    H2="student choice 2",
    H3="student choice 3",
    H4="student choice 4",
    H5="student choice 5",
    H6="student choice 6",
    H7="student choice 7",
    H8="student choice 8",
    H9="student choice 9",
    H10="student choice 10",
    H11="student choice 11",
    H12="student choice 12",
)

Plotting data¶

We now have a nice, tidy data set to work with. Let's start making some plots.

Temperature check¶

First, we'll plot the temperature versus time to make sure there we no problems with the temperature setting in the plate reader.

In [8]:
p = bokeh.plotting.figure(
    frame_width=400,
    frame_height=200,
    x_axis_label='time (hr)',
    y_axis_label='temperature (deg. C)',
    x_range=[0, 24],
)

p.line(x=df['time (hr)'], y=df['temperature (deg C)'], line_width=2)

bokeh.io.show(p)

Within 12 minutes, the temperature game to the set point of 37 degrees and remained stable, so we are in good shape.

Checking blanks and computing the background¶

Next, let's look at the background traces. We will plot all time series for the blanks, and then overlay the average blank,which we will store as the background.

In [9]:
# Background traces
p = bokeh.plotting.figure(
    frame_width=400,
    frame_height=200,
    x_axis_label='time (hr)',
    y_axis_label='absorbance',
    x_range=[0, 24],
)

# Plot all background traces
for well in conditions['blank']:
    p.line(x=df['time (hr)'], y=df[well])

# Compute background by averaging background values at each time point
df['background'] = df[conditions['blank']].mean(axis=1)

# Overlay average blank
p.line(x=df['time (hr)'], y=df['background'], color='tomato', line_width=2)

bokeh.io.show(p)

All of the blanks are more or less steady, with a typical absorbance of 0.085.

Plotting growth curves in pure sugars¶

Now, we can begin plotting the background-subtracted absorbance. Let us start with a simple example, pure glocose.

In [10]:
# Instantiate figure
p = bokeh.plotting.figure(
    frame_width=400,
    frame_height=200,
    x_axis_label='time (hr)',
    y_axis_label='absorbance',
    x_range=[0, 24],
)

# Loop through and add a line for each pure glucose trace
for well in conditions['glucose']:
    p.line(x=df['time (hr)'], y=df[well] - df['background'])

bokeh.io.show(p)

This is interesting. We immediately see exponential growth, which ends at about the six hour mark, when the bacterial abruptly stop growing. There is a pause for a couple hours, another growth phase for about five hours, and then a slow decline. We will postulate in class why there may be an extra growth phase.

We would like to compare with the other pure sugar sources as well, so let's include galactose, sorbitol, and xylose in our plot. We will color galactose red, sorbitol orange, and xylose purple.

In [11]:
# Loop through and add a line for each pure galactose trace
for well in conditions['galactose']:
    p.line(x=df['time (hr)'], y=df[well] - df['background'], color='orange')
    
# Loop through and add a line for each pure sorbitol trace
for well in conditions['sorbitol']:
    p.line(x=df['time (hr)'], y=df[well] - df['background'], color='green')

# Loop through and add a line for each pure xylose trace
for well in conditions['xylose']:
    p.line(x=df['time (hr)'], y=df[well] - df['background'], color='tomato')

bokeh.io.show(p)

All curves appear to show exponential growth. Glucose, xylose, and sorbitol all then have a pause, and then secondary growth and then decay.

This plot is fine and good, but it leaves a bit to be desired. First, we would like to have the absorbance axis on a logarithmic scale so we can better see the exponential growth; it will appear as a line, with the growth rate being the slope. Second, we would like to have a legend, so we do not have to just remember which curve was which. To get a logarithmic vertical axis, we use the y_axis_type='log' keyword argument when we instantiate the plot. There are many ways to get a legend; I prefer hand-building it and then placing it outside the plot. We can also make the legend clickable, so that we can show and hide specific curves. The code below accomplishes all of this.

In [12]:
# Instantiate figure
p = bokeh.plotting.figure(
    frame_width=400,
    frame_height=200,
    x_axis_label='time (hr)',
    y_axis_label='absorbance',
    y_axis_type='log',
    x_range=[0, 24],
    toolbar_location='above',
)

# Items we will place in the legend
legend_items = []

# Loop through conditions with pretty colors
for condition, color in zip(['glucose', 'galactose', 'sorbitol', 'xylose'], colorcet.b_glasbey_category10):
    # Instantiate plotted lines for a given sugar
    lines = []

    # Loop through each well corresponding to the sugar
    for well in conditions[condition]:
        # A line of the time series of absorbance
        lines.append(p.line(x=df['time (hr)'], y=df[well] - df['background'], line_color=color))

    # Add the lines to the legend
    legend_items.append((condition, lines))

# Create the legend from the items
legend = bokeh.models.Legend(items=legend_items, click_policy="hide")

# Add the legend to the plot
p.add_layout(legend, "right")

bokeh.io.show(p)

This plot is clear. There is about a 2.5-hour lag phase for glucose before exponential growth kicks in. The lag is a bit longer for sorbitol and xylose. Sorbitol and xylose seem to have bacteria with similar growth rates, which are smaller than that in glucose. We will quantify these more precisely when we do regressions in a moment.

Plotting multiple sugar sources¶

Let us now look at plots of growth curves in the presence of two sugars. We will look at glucose and sorbitol to start with.

It is good practice to put all of the curves on the same plot so that they can be easily compared. Since we want to do this for multiple sugar sources, we can write a function to generate the plots.

In [13]:
def plot_sugar_source(df, sugar_source, y_axis_type='log'):
    """Make a plot of all glucose + sugar_source conditions"""
    # Instantiate figure
    p = bokeh.plotting.figure(
        frame_width=450,
        frame_height=250,
        x_axis_label="time (hr)",
        y_axis_label="absorbance",
        y_axis_type=y_axis_type,
        x_range=[0, 24],
        toolbar_location="above",
    )

    # Conditions to consider
    conds = ["glucose", sugar_source] + [
        f"{sugar_source}:glucose {ratio}" for ratio in ["1:4", "1:1", "4:1"]
    ]

    # Color glucose in blue, pure other sugar in orange, grays for ratios
    colors = ["#1f77b4", "orange"] + list(bokeh.palettes.Greys5[1:-1])

    # Populate glyphs, legend items
    legend_items = []
    for cond, color in zip(conds, colors):
        lines = []
        for well in conditions[cond]:
            lines.append(
                p.line(
                    x=df["time (hr)"],
                    y=df[well] - df["background"],
                    line_color=color,
                )
            )
        legend_items.append((cond, lines))

    # Build and add legend
    legend = bokeh.models.Legend(items=legend_items, click_policy="hide")
    p.add_layout(legend, "right")

    return p

Let's take this for a spin! We'll use sorbitol.

In [14]:
bokeh.io.show(plot_sugar_source(df, 'sorbitol'))

This plot clearly shows diauxic growth! All curves follow glucose at short times, and then when the glucose is eaten, they switch to sorbitol with a different slope.

Obtaining the growth rates¶

We learned how to do a nonlinear regression in a previous tutorial. Here, we need to extract the region of the growth curve of interest (exponential growth phase) and then directly what we applied in the previous tutorial.

Extracting data of interest¶

To extract data of interest from a data frame, we can use Boolean indexing. Let's look at the pure glucose curves, shown again here.

In [15]:
# Instantiate figure
p = bokeh.plotting.figure(
    frame_width=400,
    frame_height=200,
    x_axis_label='time (hr)',
    y_axis_label='absorbance',
    y_axis_type='log',
    x_range=[0, 24],
)

# Loop through and add a line for each pure glucose trace
for well in conditions['glucose']:
    p.line(x=df['time (hr)'], y=df[well] - df['background'])

bokeh.io.show(p)

In looking at the graph, exponential growth phase is between three and five hours. To extract those parts of the curve, we use the following syntax. Be sure to note the use of parentheses and the & operator.

In [16]:
sub_df = df.loc[(df['time (hr)'] >= 3) & (df['time (hr)'] <= 5), :]

Now, we have a sub-data frame that only has time points between three and five hours.

Performing a regression to get the growth rate¶

Let us now consider one of the glucose curves and perform a curve fit. As in the first tutorial, we need to define a theoretical function for exponential growth. Since we are considering background-substracted growth, the equation is

\begin{align} A = A_0\,\mathrm{e}^{r t}, \end{align}

where $A$ is absorbance and $r$ is the growth rate.

In [17]:
# Define exponential growth function
def exp_growth(t, A0, r):
    return A0 * np.exp(r * t)

Next, get guess parameters.

In [18]:
# Parameter guesses
A0_guess = 0.01
r_guess = 1.0 # 1/hr

Next, we pull out the time points and absorbance measurements, making sure to do background subtraction.

In [19]:
# Pull out time points and absorbances
t = sub_df['time (hr)'].values
A = (sub_df[conditions['glucose'][0]] - sub_df['background']).values

Finally, we use scipy.optimize.curve_fit() to get the best-fit parameters.

In [20]:
# Perform optimization
popt, _ = scipy.optimize.curve_fit(exp_growth, t, A, p0=(A0_guess, r_guess))

# Extract parameters
A0, r = popt

# What was our growth rate?
print(f"growth rate = {r} inverse hours")
growth rate = 0.667059967805969 inverse hoursIntel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.

A growth rate of 0.67 inverse hours corresponds to a division time of $\ln 2 / 0.67 = 1$ hour.

Let's overlay a plot.

In [21]:
# Generator theoretical curve, going a bit beyond
# both sides of curve fit region
t_theor = np.linspace(1, 6, 200)
A_theor = exp_growth(t_theor, *popt)

# Add to plot
p.line(t_theor, A_theor, line_color='tomato')

bokeh.io.show(p)

Looks nice!

Growth rates for single sugars¶

We should automate the regression by writing a function to perform it.

In [22]:
def fit_growth(df, well, t_start, t_end):
    """Obtain estimates for A0 and r for a given sub-trace"""
    # Parameter guesses
    A0_guess = 0.01
    r_guess = 1.0  # 1/hr

    # Pull out data
    sub_df = df.loc[(df["time (hr)"] >= t_start) & (df["time (hr)"] <= t_end), :]
    t = sub_df["time (hr)"].values
    A = (sub_df[well] - sub_df["background"]).values

    # Perform optimization
    popt, _ = scipy.optimize.curve_fit(exp_growth, t, A, p0=(A0_guess, r_guess))

    return popt

We can now use this function to get growth rates for all of the pure sugars. First, we will specify the start and end times for exponential growth for each based on eyeballing the plots above.

In [23]:
time_windows = dict(
    glucose=[3, 5],
    galactose=[3, 19],
    sorbitol=[3, 8],
    xylose=[3, 8],
)

Now we can perform the regressions to get the growth rates. We will store each result as a dictionary containing the sugar source, trial number, and value of the growth rate from the regression. Then, we can convert a list of these results dictionaries into a data frame for easy viewing and plotting.

In [24]:
results = []
for sugar in ['glucose', 'galactose', 'sorbitol', 'xylose']:
    for trial, well in enumerate(conditions[sugar]):
        A0, r = fit_growth(df, well, *time_windows[sugar])
        results.append(dict(sugar=sugar, trial=trial, A0=A0, r=r))

# Make a data frame from the list of results
df_growth_rates = pd.DataFrame(results)

# Take a look
df_growth_rates
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Out[24]:
sugar trial A0 r
0 glucose 0 0.006727 0.667060
1 glucose 1 0.006275 0.683410
2 galactose 0 0.010666 0.158853
3 galactose 1 0.011758 0.158613
4 sorbitol 0 0.006524 0.459459
5 sorbitol 1 0.006654 0.462238
6 xylose 0 0.004304 0.486919
7 xylose 1 0.004488 0.485660
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.

If we like, we can summarize our results in a plot.

In [25]:
p = iqplot.strip(
    df_growth_rates,
    q="r",
    cats="sugar",
    q_axis='y',
    y_axis_label="growth rate (1/hr)",
    spread="swarm",
    y_range=[0, 0.7],
)

bokeh.io.show(p)

This provides a nice look into the growth rates.

As a check, we should make sure the curve fits look ok. We can again overlay them on the plot. It's a fair amount of code to do it, but it's straightforward. We will write it as a function, since we will use this again and again.

In [26]:
def plot_multi(
    df,
    conds,
    colors=colorcet.b_glasbey_category10,
    show_regression=False,
    store_regression=False,
    time_windows=None,
):
    """Generate a plot with multiple conditions, with or without
    showing regression lines."""
    # Instantiate figure
    p = bokeh.plotting.figure(
        frame_width=400,
        frame_height=200,
        x_axis_label="time (hr)",
        y_axis_label="absorbance",
        y_axis_type="log",
        x_range=[0, 24],
        toolbar_location="above",
    )

    # Items we will place in the legend
    legend_items = []

    # Loop through conditions with pretty colors
    for cond, color in zip(conds, colors):
        # Instantiate plotted lines for a given condition
        lines = []

        # Loop through each well corresponding to the sugar
        for well in conditions[cond]:
            # A line of the time series of absorbance
            lines.append(
                p.line(
                    x=df["time (hr)"], y=df[well] - df["background"], line_color=color
                )
            )

        # Add the lines to the legend
        legend_items.append((cond, lines))

    if show_regression:
        # Loop through an add best-fit lines
        fit_lines = []
        for cond in conds:
            # Loop through each well corresponding to the sugar
            for trial, well in enumerate(conditions[cond]):
                # Convert time window to a list of time windows if necessary
                if type(time_windows[cond][0]) != list:
                    t_windows = [time_windows[cond]]
                else:
                    t_windows = time_windows[cond]

                # Get best fit curve for each time window
                for t_window in t_windows:
                    # Get best fit parameters
                    A0, r = fit_growth(df, well, *t_window)

                    # Time points for theoretical curve
                    t_theor = np.linspace(*t_window, 200)

                    # Theoretical absorbance
                    A_theor = exp_growth(t_theor, A0, r)

                    # Plot theoretical curve in gray
                    fit_lines.append(p.line(x=t_theor, y=A_theor, line_color="gray"))

        # Add the fit lines to the legend
        legend_items.append(("regression", fit_lines))

    # Create the legend from the items
    legend = bokeh.models.Legend(items=legend_items, click_policy="hide")

    # Add the legend to the plot
    p.add_layout(legend, "right")

    return p

Let's now make our plot to do the check!

In [27]:
bokeh.io.show(
    plot_multi(
        df,
        ["glucose", "galactose", "sorbitol", "xylose"],
        show_regression=True,
        time_windows=time_windows,
    )
)
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.

Fitting diauxic curves¶

Fitting a diauxic curve is a bit more challenging, since we need to look carefully at the respective exponential growth regimes. Let's consider again the sorbitol curve as an example. We first need to add the time windows to the dictionary.

In [28]:
time_windows['sorbitol:glucose 1:4'] = [[3.5, 5], [6.2, 6.4]]
time_windows['sorbitol:glucose 1:1'] = [[3, 4.2], [5.7, 6.2]]
time_windows['sorbitol:glucose 4:1'] = [[2, 3.2], [4.2, 6.4]]

Now, we can make the curve fits.

In [29]:
bokeh.io.show(
    plot_multi(
        df,
        ["glucose", "sorbitol", 'sorbitol:glucose 1:4', 'sorbitol:glucose 1:1', 'sorbitol:glucose 4:1'],
        show_regression=True,
        colors=['#1f77b4', 'orange'] + list(bokeh.palettes.Purples5[1:-1]),
        time_windows=time_windows,
    )
)
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.

The regressions all look good! Now, let's do all of the regressions, collect them and compare!

In [30]:
conds = ["glucose", "sorbitol", 'sorbitol:glucose 1:4', 'sorbitol:glucose 1:1', 'sorbitol:glucose 4:1']
results = []

# Loop through conditions
for cond in conds:
    # Convert time windows to list of lists if necessary
    if type(time_windows[cond][0]) != list:
        t_windows = [time_windows[cond]]
    else:
        t_windows = time_windows[cond]

    # Loop through each trial
    for trial, well in enumerate(conditions[cond]):
                    
        # Loop through each diauxic phase
        for diaux_phase, t_window in enumerate(t_windows):
            A0, r = fit_growth(df, well, *t_window)
            results.append(dict(condition=cond, trial=trial, diaux_phase=diaux_phase, A0=A0, r=r))

# Make a data frame from the list of results
df_sorb = pd.DataFrame(results)

# Take a look
df_sorb
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Out[30]:
condition trial diaux_phase A0 r
0 glucose 0 0 0.006727 0.667060
1 glucose 1 0 0.006275 0.683410
2 sorbitol 0 0 0.006524 0.459459
3 sorbitol 1 0 0.006654 0.462238
4 sorbitol:glucose 1:4 0 0 0.007230 0.651689
5 sorbitol:glucose 1:4 0 1 0.025639 0.367873
6 sorbitol:glucose 1:4 1 0 0.006049 0.672106
7 sorbitol:glucose 1:4 1 1 0.019600 0.403852
8 sorbitol:glucose 1:1 0 0 0.005685 0.684604
9 sorbitol:glucose 1:1 0 1 0.012195 0.471591
10 sorbitol:glucose 1:1 1 0 0.005655 0.703265
11 sorbitol:glucose 1:1 1 1 0.012643 0.471652
12 sorbitol:glucose 4:1 0 0 0.007424 0.619212
13 sorbitol:glucose 4:1 0 1 0.011168 0.480342
14 sorbitol:glucose 4:1 1 0 0.007000 0.631984
15 sorbitol:glucose 4:1 1 1 0.010908 0.481002
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.

Looks good; now let's make a plot!

In [31]:
# Specify the order of the conditions
order = (
    ("glucose", 0),
    ("sorbitol", 0),
    ("sorbitol:glucose 1:4", 0),
    ("sorbitol:glucose 1:4", 1),
    ("sorbitol:glucose 1:1", 0),
    ("sorbitol:glucose 1:1", 1),
    ("sorbitol:glucose 4:1", 0),
    ("sorbitol:glucose 4:1", 1),
)

# Colors
colors = ['#1f77b4', 'orange'] * 4

# Make a strip plot of the growth rates
p = iqplot.strip(
    df_sorb,
    q="r",
    cats=["condition", "diaux_phase"],
    y_axis_label="growth rate (1/hr)",
    spread="swarm",
    q_axis="y",
    order=order,
    palette=colors,
    frame_width=500,
    y_range=[0, 0.8],
)

bokeh.io.show(p)

For the sorbitol:glucose 1:4 ratio, we barely had any points to fit the second diauxic phase of growth, so that fit is not terribly reliable. We see that, indeed, all cells seem to be growing on glucose at the same rate as in pure glucose, but as glucose runs out, the cells grow at the same rate as in pure sorbitol.

Computing environment¶

In [32]:
%load_ext watermark
%watermark -v -p  numpy,scipy,pandas,bokeh,colorcet,bi1x,jupyterlab
Python implementation: CPython
Python version       : 3.12.2
IPython version      : 8.20.0

numpy     : 1.26.4
scipy     : 1.12.0
pandas    : 2.2.1
bokeh     : 3.4.0
colorcet  : 3.1.0
bi1x      : 0.0.14
jupyterlab: 4.0.11