Chris Parmer — home

Michaelis-Menten plot

Biochemistry — Puromycin dataset

Example from the compendium of canonical charts

Biochemistry — Michaelis-Menten plot (Puromycin dataset)

Python Code

"""Biochemistry — Michaelis-Menten plot (Puromycin dataset)."""


import numpy as np
from scipy.optimize import curve_fit
import plotly.graph_objects as go

URL = "https://vincentarelbundock.github.io/Rdatasets/csv/datasets/Puromycin.csv"


def michaelis_menten(S, Vmax, Km):
    return Vmax * S / (Km + S)


def generate():
    print("fetching Puromycin data …")
    used_synthetic = False
    try:
        df = fetch_csv(URL)
        print(f"  cols: {list(df.columns)}, shape: {df.shape}")
        # Column names may vary
        conc_col = [c for c in df.columns if "conc" in c.lower()][0]
        rate_col = [c for c in df.columns if "rate" in c.lower()][0]
        state_col = [c for c in df.columns if "state" in c.lower()][0]
        df = df[[conc_col, rate_col, state_col]].dropna()
        df.columns = ["conc", "rate", "state"]
    except Exception as e:
        print(f"  fetch failed ({e}); using synthetic Puromycin-like data")
        used_synthetic = True
        import pandas as pd
        # Typical Puromycin data (counts/min/min vs ppm)
        conc_t = [0.02, 0.02, 0.06, 0.06, 0.11, 0.11, 0.22, 0.22, 0.56, 0.56, 1.10, 1.10]
        rate_t = [76, 47, 97, 107, 123, 139, 159, 152, 191, 201, 207, 200]
        conc_u = [0.02, 0.02, 0.06, 0.06, 0.11, 0.11, 0.22, 0.22, 0.56, 0.56, 1.10, 1.10]
        rate_u = [67, 51, 84, 86, 98, 115, 131, 124, 144, 158, 160, 183]
        df = pd.DataFrame({
            "conc": conc_t + conc_u,
            "rate": rate_t + rate_u,
            "state": ["treated"] * len(conc_t) + ["untreated"] * len(conc_u),
        })

    states = df["state"].unique().tolist()
    colors = {"treated": VIOLET, "untreated": TEAL}
    # fallback for unexpected state names
    if len(states) == 2:
        color_map = {states[0]: VIOLET, states[1]: TEAL}
    else:
        color_map = {s: c for s, c in zip(states, [VIOLET, TEAL])}

    fig = go.Figure()
    conc_dense = np.linspace(0, df["conc"].max() * 1.1, 300)

    annotations = []

    for state in states:
        sub = df[df["state"] == state]
        S = sub["conc"].values
        v = sub["rate"].values
        color = color_map[state]

        # Fit
        try:
            p0 = [v.max(), np.median(S)]
            popt, _ = curve_fit(michaelis_menten, S, v, p0=p0, maxfev=5000)
            Vmax, Km = popt
            print(f"  {state}: Vmax = {Vmax:.1f}, Km = {Km:.4f}")
        except Exception as e:
            print(f"  fit failed for {state}: {e}; using rough estimate")
            Vmax = v.max() * 1.1
            Km = np.median(S)

        v_fit = michaelis_menten(conc_dense, Vmax, Km)

        # Scatter data points
        fig.add_trace(go.Scatter(
            x=S,
            y=v,
            mode="markers",
            name=state.capitalize(),
            marker=dict(color=color, size=9, symbol="circle"),
            legendgroup=state,
            hovertemplate=f"[S]=%{{x:.3f}} ppm<br>v=%{{y:.0f}} counts/min/min<extra>{state}</extra>",
        ))

        # Fitted hyperbola
        fig.add_trace(go.Scatter(
            x=conc_dense,
            y=v_fit,
            mode="lines",
            name=f"{state.capitalize()} fit",
            line=dict(color=color, width=2, dash="dot"),
            legendgroup=state,
            showlegend=False,
            hoverinfo="skip",
        ))

        # Annotation with Vmax and Km
        ann_x = Km * 3  # somewhere on the rising part of curve
        ann_y = michaelis_menten(ann_x, Vmax, Km)
        offset_y = 30 if state == states[0] else -50
        annotations.append(dict(
            x=ann_x,
            y=ann_y,
            text=f"{state.capitalize()}<br>Vmax = {Vmax:.0f}<br>Km = {Km:.3f} ppm",
            showarrow=True,
            arrowhead=2,
            ax=60,
            ay=offset_y,
            font=dict(size=11, color=color),
            bgcolor="rgba(255,255,255,0.85)",
            bordercolor=color,
            borderwidth=1,
        ))

    fig.update_layout(
        xaxis=dict(title="[S] (ppm)"),
        yaxis=dict(title="Rate (counts/min/min)"),
        legend=dict(orientation="h", y=1.05, x=0),
        annotations=annotations,
        margin=dict(t=50, b=60, l=80, r=60),
    )
    return fig


if __name__ == "__main__":
    generate()

Made with Plotly