Example 1 — Basic logistic fitting#
Fits a logistic growth model to synthetic plate-reader data, plots observed vs fitted curves, and compares two models by AICc.
Full script: examples/scripts/02_nl_fitting.py
Setup#
import numpy as np
import matplotlib.pyplot as plt
import pykinbiont
from pykinbiont import GrowthData, FitOptions, ModelSpec, MODEL_REGISTRY, fit
pykinbiont.configure("/path/to/KinBiont.jl") # skip if using registry version
Generate synthetic data#
def logistic(t, K=1.2, mu=0.5, N0=0.01):
return K / (1 + ((K - N0) / N0) * np.exp(-mu * t))
rng = np.random.default_rng(1)
times = np.linspace(0, 20, 80)
curves = np.stack([
np.maximum(
logistic(times, K=1.0 + 0.1 * i, mu=0.4 + 0.05 * i, N0=0.01)
+ rng.normal(0, 0.007, len(times)),
1e-4, # clip to avoid log(negative) errors
)
for i in range(4)
])
data = GrowthData(
curves=curves,
times=times,
labels=["W1", "W2", "W3", "W4"],
)
Why clip to 1e-4?
KinBiont computes log(OD) internally to estimate growth rates. Any OD ≤ 0 raises a
DomainError. Clipping Gaussian noise at a small positive floor prevents this.
Fit with logistic model#
logistic_model = MODEL_REGISTRY["NL_logistic"]
spec = ModelSpec(
models=[logistic_model],
params=[[1.2, 0.01, 0.5]], # [N_max, N0, mu]
lower=[[0.3, 1e-3, 0.05]],
upper=[[3.0, 0.05, 3.0]],
)
opts = FitOptions(smooth=True, smooth_method="rolling_avg", multistart=True, n_restart=20)
results = fit(data, spec, opts)
Inspect results#
df = results.to_dataframe()
print(df[["label", "best_model", "aic", "param_1", "param_2", "param_3"]])
label |
best_model |
aic |
param_1 (N_max) |
param_2 (N0) |
param_3 (mu) |
|---|---|---|---|---|---|
W1 |
NL_logistic |
−142 |
1.007 |
0.010 |
0.401 |
W2 |
NL_logistic |
−138 |
1.103 |
0.010 |
0.449 |
Plot#
fig, axes = plt.subplots(2, 2, figsize=(9, 6))
for ax, r in zip(axes.flat, results):
idx = data.labels.index(r.label)
ax.plot(data.times, data.curves[idx], ".", markersize=3, alpha=0.5, label="data")
ax.plot(r.times, r.fitted_curve, "-", lw=2, label="logistic fit")
ax.set_title(r.label)
ax.set_xlabel("Time (h)")
ax.set_ylabel("OD")
ax.legend(fontsize=7)
plt.tight_layout()
plt.savefig("logistic_fits.png", dpi=150)
Two-model comparison#
gompertz_model = MODEL_REGISTRY["NL_gompertz"]
spec_both = ModelSpec(
models=[logistic_model, gompertz_model],
params=[[1.2, 0.01, 0.5], [1.2, 0.5, 1.0]],
lower=[[0.3, 1e-3, 0.05], [0.3, 0.0, 0.0]],
upper=[[3.0, 0.05, 3.0], [3.0, 3.0, 10.0]],
)
results_both = fit(data, spec_both, opts)
print(results_both.to_dataframe()[["label", "best_model", "aic"]])
The model with the lower AICc is selected automatically per curve.