Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Overview

This guide explains how to analyze time-series fluorescence data from plate reader expression experiments using the open-source Nucleus Cell Development Kit (CDK). Here, we’ll cover how to load, normalize, visualize, and fit your data (see DevNote), as well as describe the resultin summary statistics.

The CDK is available on PyPi for install on your own computer (requires Python 3.11+ and the poetry package).

1. Setup

First, import the necessary libraries. The platereader module from the CDK contains specialized functions for loading plate reader data, performing kinetic analysis, and visualization. By convention, we import platereader as pr.

%load_ext autoreload
%autoreload 2

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

# Import the cdk platereader module
from cdk.instruments import platereader as pr

2. Load Data

Load your plate reader output and merge it with the platemap (see DevNote) that describes your experimental conditions.

Call load_platereader_data() to parse the file:

# Specify file paths
data_file = "path/to/data.txt"
platemap_file = "path/to/platemap.csv"

# Load data
result = pr.load_platereader_data(
    data_file=data_file,
    platemap_file=platemap_file,
    platereader="biotek"
)

The output is a PlateReaderResult object: a list-like collection of PlateReaderData blocks. If you did more than one read on the plate (multiple gains, or different ex/em spectra), each read is a separate block. Print result to see what’s inside:

print(result)
PlateReaderResult with the following 2 blocks:
0: (1533, 54) kinetic read with reads: GFP-G70:485,528 (Fluorescence) (Plate 'Plate 1')
1: (1533, 54) kinetic read with reads: GFP-Gext:485,528 (Fluorescence) (Plate 'Plate 1')

The output lists all of the blocks in the data file. For each block, you can see its index, the type of experiment stored (here, kinetic), the dimensions of the underlying DataFrame, the reads recorded and their measurement modalities, and the ID of the plate used.

Here, there are two reads of a single plate with different gains and the same excitation/emission spectra. To work with a single read, choose its index (e.g., the GFP-Gext read is block index 1):

desired_index = 1
data = result[desired_index]

You can see the underlying data with data.view() (which returns a Pandas DataFrame):

# Show the first five rows:
data.view().info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1533 entries, 0 to 1532
Data columns (total 42 columns):
 #   Column                      Non-Null Count  Dtype          
---  ------                      --------------  -----          
 0   Date                        1533 non-null   datetime64[ns] 
 1   Experiment                  1533 non-null   object         
 2   Well                        1533 non-null   object         
 3   Name                        1533 non-null   object         
 4   Type                        1533 non-null   object         
 5   Time                        1533 non-null   timedelta64[ns]
 6   Data                        1533 non-null   int64          
 7   Data_normalized             1533 non-null   float64        
 8   Read                        1533 non-null   object         
 9   Read Name                   1533 non-null   object         
 10  Reader Type                 1533 non-null   object         
 11  Reader ID                   1533 non-null   object         
 12  Plate Type                  1533 non-null   object         
 13  Plate ID                    1533 non-null   object         
 14  Start Time                  1533 non-null   datetime64[ns] 
 15  Read Modality               1533 non-null   object         
 16  Gain                        1533 non-null   object         
 17  Excitation Wavelength (nm)  1533 non-null   int64          
 18  Excitation Bandwidth (nm)   1533 non-null   object         
 19  Excitation Optics           1533 non-null   object         
 20  Emission Wavelength (nm)    1533 non-null   int64          
 21  Emission Bandwidth (nm)     1533 non-null   int64          
 22  Emission Optics             1533 non-null   object         
 23  Read Geometry               1533 non-null   object         
 24  Read Height (mm)            1533 non-null   object         
 25  PMix ID                     1314 non-null   object         
 26  [PMix] (mg/mL)              0 non-null      float64        
 27  Ribosome ID                 0 non-null      float64        
 28  [Ribosome] (uM)             0 non-null      float64        
 29  SMS ID                      1314 non-null   object         
 30  tRNA ID                     1095 non-null   object         
 31  [tRNA] (ug/uL)              1095 non-null   float64        
 32  DNA ID                      1314 non-null   object         
 33  [DNA] (ng/uL)               1314 non-null   float64        
 34  PMix Vol (uL)               1314 non-null   float64        
 35  Ribosome Vol (uL)           0 non-null      float64        
 36  SMS Vol (uL)                1314 non-null   float64        
 37  tRNA Vol (uL)               1314 non-null   float64        
 38  DNA Vol (uL)                1314 non-null   float64        
 39  RNase Inhib Vol (uL)        1314 non-null   float64        
 40  Water vol (uL)              1314 non-null   float64        
 41  Rxn Volume (uL)             1314 non-null   float64        
dtypes: datetime64[ns](2), float64(14), int64(4), object(21), timedelta64[ns](1)
memory usage: 503.1+ KB

The dataset contains information from the platemap and metadata extracted from the plate reader’s output, in long format. Each row represents a single measurement, where:


3. Plot Raw Curves

First, visualize your data. You can do this by calling data.plot(). By default, one curve is plotted for each distinct Name in the platemap. The band shows a bootstrapped 95% confidence interval of the mean across wells with that Name.

Passing style='Type' assigns different line styles for the different sample types that appear in your platemap (Sample, Standard, Blank, etc. as defined in the platemap standard). Line styles by type makes it easy to spot controls, catch outliers, failed reactions, or unexpected behavior before fitting dat.

data.plot(style='Type')
png

We can see that our Samples show typical behavior and that the standard (here, HPTS) is stable, thus we can safely normalize our data.


4. Normalize Data

Instead of performing your analyses on the raw units output by the plate reader, we recommend first normalizing to a standard so values are comparable across experiments and instruments.

The function data.normalize('<standard name>') takes the time average of each well labeled as a Standard at the end of the run (1 hour by default), then divides all data by that mean.

The name you pass as an argument to data.normalize() must match the standard’s Name column from your platemap. You can check the name of your standards this way:

platemap = data.platemap
standards = platemap[platemap['Type']=='Standard']
standards['Name'].unique()
array(['10 uM HPTS'], dtype=object)

Then, call data.normalize():

data = data.normalize('10 uM HPTS')

NOTE: you need to save the output of this transformation — it does not happen in-place!

Now replot to see your normalized data. We exclude the standard from the plot using exclude_types=['Standard']:

g = data.plot(style='Type', exclude_types=['Standard'])
# To save the output:
# g.savefig('after-normalization.png',dpi=300)
png

The plot y-axis label will automatically update to indicate that your data have been normalized and will indicate the name of the standard used.


5. Kinetic Analysis

Now we’re ready to do our kinetic analysis.

data.fit_kinetics() fits a sigmoid-with-drift curve to each well (grouped by unique well identifiers: Experiment, Well, Read, and Reader ID by default) and extracts interpretable kinetic parameters.

The model is:

y(t)=L1+ek(tτvmax)+b(tτdrift)y(t) = \frac{L}{1 + e^{-k(t - \tau_{v_\text{max}})}} + b\,(t - \tau_\text{drift})

with parameters:

See the DevNote on kinetic analysis for more details.

Metrics extracted:

Run the kinetic analysis:

kinetics = data.fit_kinetics()

fit_kinetics() returns a Kinetics object. Its .summary property is a tidy table of the fitted parameters and quality metrics per group (all-null columns dropped):

kinetics.summary
WellNameMax VelocityMax Velocity TimeLag TimeSteady StateCompletion TimeCompletion ThresholdDriftFit FunctionR^2Good FitNormalized to
0B2tRNA AR-8362.490 days 01:33:150 days 00:33:094.740 days 03:01:440.950.07sigmoid_drift1.00True10 uM HPTS
1B4tRNA AR-8372.670 days 01:33:350 days 00:34:384.990 days 03:00:240.950.07sigmoid_drift1.00True10 uM HPTS
2B6tRNA AR-8382.440 days 01:32:350 days 00:34:214.500 days 02:58:170.950.06sigmoid_drift1.00True10 uM HPTS
3B8tRNA AR-8391.680 days 01:32:000 days 00:37:352.890 days 02:52:070.950.03sigmoid_drift1.00True10 uM HPTS
4B10tRNA AR-730 (Rxn Control)2.370 days 01:30:010 days 00:33:504.220 days 02:52:450.950.07sigmoid_drift1.00True10 uM HPTS
5B12NEB Positive Control2.660 days 01:35:500 days 00:45:094.270 days 02:50:260.950.09sigmoid_drift1.00True10 uM HPTS
6D2tRNA AR-8362.640 days 01:31:440 days 00:32:124.970 days 02:59:230.950.08sigmoid_drift1.00True10 uM HPTS
7D4tRNA AR-8372.790 days 01:32:130 days 00:34:275.100 days 02:57:160.950.07sigmoid_drift1.00True10 uM HPTS
8D6tRNA AR-8382.520 days 01:32:000 days 00:34:314.580 days 02:56:370.950.06sigmoid_drift1.00True10 uM HPTS
9D8tRNA AR-8391.840 days 01:30:320 days 00:35:413.200 days 02:51:170.950.04sigmoid_drift1.00True10 uM HPTS
10D10tRNA AR-730 (Rxn Control)2.410 days 01:28:510 days 00:32:204.310 days 02:52:030.950.07sigmoid_drift1.00True10 uM HPTS
11D12NEB Positive Control3.130 days 01:34:330 days 00:44:144.990 days 02:48:380.950.09sigmoid_drift1.00True10 uM HPTS
12F2tRNA AR-8362.790 days 01:30:120 days 00:32:105.120 days 02:55:380.950.08sigmoid_drift1.00True10 uM HPTS
13F4tRNA AR-8372.770 days 01:31:330 days 00:33:225.100 days 02:57:120.950.07sigmoid_drift1.00True10 uM HPTS
14F6tRNA AR-8382.490 days 01:30:490 days 00:33:354.510 days 02:55:050.950.06sigmoid_drift1.00True10 uM HPTS
15F8tRNA AR-8391.790 days 01:28:450 days 00:36:412.950 days 02:45:250.950.03sigmoid_drift1.00True10 uM HPTS
16F10tRNA AR-730 (Rxn Control)2.460 days 01:28:120 days 00:32:054.370 days 02:50:490.950.07sigmoid_drift1.00True10 uM HPTS
17F12NEB Positive Control2.520 days 01:33:540 days 00:44:033.980 days 02:47:180.950.08sigmoid_drift1.00True10 uM HPTS

Visualizing fits

The function kinetics.plot() overlays each fitted curve on its raw data so you can confirm the fits are reasonable (high R², smooth curves).

Across replicates
Individual wells

By default, kinetics.plot() facets by Name, where traces of replicate wells of each condition are shown in a single panel with an average fit overlaid:

g = kinetics.plot()
png

6. Summary Plots

kinetics.plot_summary() produces a multi-panel figure comparing kinetic parameters across conditions: per-experiment time series, in addition to bar plots of steady-state, max velocity, and drift with error bars across technical replicates.

g = kinetics.plot_summary()
# To save:
# g.savefig("data_summary.png", dpi=300)
png

Tips and Troubleshooting

  • Overflow errors: Wells with OVRFLW or NaN values are automatically excluded from fitting

  • Poor fits (low R²): Inspect raw curves for anomalies (bubbles, evaporation, pipetting errors)

  • Drift: Sometimes seen in kinetics curves; the default sigmoid_drift model accounts for it

  • Multiple replicates: Always include technical replicates and report error bars

  • Comparing conditions: Normalize or blank data consistently across all samples