<jupyter_start><jupyter_text>First Growth Curves<jupyter_code>import numpy as np 
import pandas as pd
import gcutils
import altair as alt
import imp
import requests
from io import BytesIO
imp.reload(gcutils)
import scipy.stats
import matplotlib.pyplot as plt
# Load custom plotting style and colors
colors, palette = gcutils.viz.matplotlib_style()
colors, palette = gcutils.viz.altair_style()<jupyter_output><empty_output><jupyter_text>This notebook contains the analysis of my first few sets of bacterial growth curves and compares the results to the "gold-standard" growth curves provided by Jonas.   January 15 Growth DataJonas shared with me a dataset of bacterial growth curves obtained on a variety of well-defined carbon substrates. I tidied up the data so it's more easily readable, which we load and plot below.<jupyter_code># Load the data 
gold_std = pd.read_csv('../../data/carbon_source_growth_data.csv')
data = pd.read_csv('../../data/2021-01-15_glucose_growth.csv')

# Convert my minute tracking to hours
data['elapsed_time_hr'] = data['elapsed_time_min'].values / 60
data.drop(columns=['elapsed_time_min'], inplace=True)
data.rename(columns={'od_600nm':'od_600'}, inplace=True)

# Drop points below 0.4 and rescale time
data = data[data['od_600'] >= 0.04]
data['elapsed_time_hr'] -= data['elapsed_time_hr'].min()

# Add identifiers
data['source'] = 'Griffin'
gold_std['source'] = 'Jonas'

# Restrict to glucose
gold_std = gold_std[gold_std['carbon_source']=='glucose']

# Merge
merged = pd.concat([data, gold_std], sort=False)

# Set up the plot base.
base = alt.Chart(merged).encode(
        x=alt.X(field='elapsed_time_hr', type='quantitative', title='elapsed time [hr]'),
        y=alt.Y(field='od_600', type='quantitative', title='optical density [a.u.]',
               scale=alt.Scale(type='log')),
        shape=alt.Shape(field='source', type='nominal'),
        color=alt.Color(field='source', type='nominal', title='data source')
)
points = base.mark_point(size=70)
points<jupyter_output><empty_output><jupyter_text>This doesn't look *too* bad, but my data (in black) definitely has a different slope than the gold-standard. On a semi-log scaling, that's not good. Let's do a simple linear regression of the log transform data to see what the slopes are<jupyter_code># Do a stupid fit for now. 
griffin = merged[merged['source']=='Griffin']
jonas = merged[merged['source']=='Jonas']
griffin_slope, griffin_int, _, _, griffin_se = scipy.stats.linregress(griffin['elapsed_time_hr'].values, np.log(griffin['od_600'].values))
jonas_slope, jonas_int, _, _, jonas_se = scipy.stats.linregress(jonas['elapsed_time_hr'].values, np.log(jonas['od_600'].values))


# Compute the fits and make a dataframe
time_range = np.linspace(0, 3.2, 200)
g_fit = griffin_int + griffin_slope * time_range
j_fit = jonas_int + jonas_slope * time_range
griffin_df = pd.DataFrame(np.array([time_range, np.exp(g_fit)]).T, columns=['time', 'y'])
griffin_df['source'] = 'Griffin'
jonas_df = pd.DataFrame(np.array([time_range, np.exp(j_fit)]).T, columns=['time', 'y'])
jonas_df['source'] = 'Jonas'
fit_merge = pd.concat([griffin_df, jonas_df], sort=False)

# Set up the figure
fit_base = alt.Chart(fit_merge).encode(
        x=alt.X(field='time', type='quantitative', title='elapsed time [hr]'),
        y=alt.Y(field='y', type='quantitative', title='optical density [a.u.]',
                scale=alt.Scale(type='log')),
        color=alt.Color(field='source', type='nominal'))

print(f"""
Growth rate (Griffin's data): λ = {griffin_slope:0.2f} ± {griffin_se:0.2f} hr^-1,
Growth rate (Jonas' data): λ = {jonas_slope:0.2f} ± {jonas_se:0.2f} hr^-1
""")
fit_curves = fit_base.mark_line(size=2, opacity=0.5)
fit_curves + points<jupyter_output>Growth rate (Griffin's data): λ = 0.71 ± 0.03 hr^-1,
Growth rate (Jonas' data): λ = 0.84 ± 0.02 hr^-1<jupyter_text>The fits look fair, let's see what the growth rate is<jupyter_code>print(f"""
Growth rate (Griffin's data): λ = {griffin_slope:0.2f} ± {griffin_se:0.2f} hr^-1,
Growth rate (Jonas' data): λ = {jonas_slope:0.2f} ± {jonas_se:0.2f} hr^-1
""")<jupyter_output>Growth rate (Griffin's data): λ = 0.69 ± 0.02 hr^-1,
Growth rate (Jonas' data): λ = 0.84 ± 0.02 hr^-1<jupyter_text>There's a substantial difference in the growth rate. According to Jonas, growth on minimal media supplemented with glucose should be closer to 0.9 or so. This may have something to do with the preculture or the actual growth media. Next week, I will try to rerun the experiment and get better growth curves.   January 18 Growth Data<jupyter_code># Load the days growth data
jan18_data = pd.read_csv('../../data/2021-01-18_glucose_growth.csv')

# Specify jonas' carbon source
gold_std['carbon_source'] = 'glucose_jonas'

# Add a source info, convert elapsed time to hr, and merge with other data
jan18_data['elapsed_time_hr'] = jan18_data['elapsed_time'].values / 60
jan18_data.rename(columns={'od_600nm':'od_600'}, inplace=True)
jan18_data['source'] = 'Griffin'

# Merge
merged = pd.concat([gold_std, jan18_data], sort=False)
base = alt.Chart(merged).encode(
        x=alt.X(field='elapsed_time_hr', type='quantitative', title='elapsed time [hr]'),
        y=alt.Y(field='od_600', type='quantitative', title='optical density [a.u.]',
               scale=alt.Scale(type='log')),
        color = alt.Color(field='source', type='nominal', title='data source'),
        shape=alt.Shape(field='carbon_source', type='nominal', title='glucose stock'))

points = base.mark_point()
points
# Compute the simple fit. 
time = np.linspace(0, 3.5, 100)
fit_stats = pd.DataFrame([])
fit_dfs = []
for g, d in merged.groupby(['source', 'carbon_source']):
    slope, inter, _, _, se = scipy.stats.linregress(d['elapsed_time_hr'].values, 
                                                    np.log(d['od_600'].values))
    fit_stats = fit_stats.append({'source':g[0], 'carbon_source':g[1], 'intercept': inter, 'slope':slope, 'sem':se},
                            ignore_index=True)
    
    # Compute the fit over elapsed time
    fit = inter + slope * time
    _df = pd.DataFrame(np.array([time, np.exp(fit)]).T, columns=['elapsed_time_hr', 'od_600'])
    _df['source'] = g[0]
    _df['carbon_source'] = g[1]
    fit_dfs.append(_df)
    print(f"{g[0]} using  '{g[1]}' glucose stock: λ = {slope:0.2f} ± {se:0.2f} hr^-1")
fit_df = pd.concat(fit_dfs)

optimal = pd.DataFrame(np.array([time, np.exp(-3.2 + 0.9 * time)]).T, columns=['elapsed_time_hr', 'od_600'])

optimal_plot = alt.Chart(optimal).encode(
                x=alt.X(field='elapsed_time_hr', type='quantitative', title=''),
                y=alt.Y(field='od_600', type='quantitative', title='',
                        scale=alt.Scale(type='log'))).mark_line(color=colors['primary_red'])
    
fit_base = alt.Chart(fit_df).encode(
            x=alt.X(field='elapsed_time_hr', type='quantitative', title='elapsed time [hr]'),
            y=alt.Y(field='od_600', type='quantitative', title='optical density [a.u.]',
                   scale=alt.Scale(type='log')),
            strokeDash='carbon_source:Q',
            color=alt.Color(field='source', type='nominal'))

fits = fit_base.mark_line()
jan18_plot = fits + points
optimal_plot + jan18_plot
colors<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.15591769092666757, 'fraction_numerical': 0.02333931777378815, 'mean_word_length': 4.587962962962963, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 20, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '5', 'dir': 'jupyter-structured-clean-dedup', 'id': '131216', 'license': 'MIT', 'n_tokens_mistral': 2733, 'n_tokens_neox': 2544, 'n_words': 606, 'path': 'code/exploratory/bayesian_parameter_estimation_lambda.ipynb', 'repo_name': 'gchure/growth_laws', 'size': '7241'}