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.

This tutorial was generated from an Jupyter notebook. You can download the notebook here.
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.
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.
- We need to wrangle the data exported from the plate reader in a text file to enable easy access and analysis.
- We need to make plots, some on semilogarithmic scale, to visualize the results.
- 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.
# 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.
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.
df = read_plate_reader(fname, header_row, last_row)
df
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).
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).
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.
conditions = invert_dict(well_metadata)
conditions
{'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.
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.
# 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.
# 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.
# 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.
# 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.
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.
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.
# 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.
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
A=A0ert,where A is absorbance and r is the growth rate.
# Define exponential growth function
def exp_growth(t, A0, r):
return A0 * np.exp(r * t)
Next, get guess parameters.
# 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.
# 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.
# 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 ln2/0.67=1 hour.
Let's overlay a plot.
# 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.
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.
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.
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.
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.
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.
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!
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.
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.
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!
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.
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!
# 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ΒΆ
%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