Modeling microbial fermentation with XGBoost

Simulating microbial growth data and using gradient-boosted trees to predict fermentation outcomes under noisy experimental conditions.

Abstract

This study combines a logistic microbial growth model with simulated experimental data, then evaluates how XGBoost captures nonlinear effects of temperature, composition, inoculum, and time.

Fermentation is essential in food production. Dough, cheese, yogurt, kombucha, and many other foods rely on the growth of microorganisms such as yeasts and lactic acid bacteria. Their growth is sensitive to several factors:

  • Temperature
  • Sugar and salt concentrations
  • pH level
  • Inoculum, or microbial quantity at the start
  • Time

However, modeling fermentation is challenging:

  • Real-world data is scarce and noisy.
  • Experiments are time-consuming.
  • The system is nonlinear and complex.

To tackle this, I used XGBoost, a high-performance implementation of gradient boosting. Rather than fitting a single model, XGBoost builds an ensemble of simple decision trees that iteratively correct each other’s errors.

This approach excels at capturing nonlinear effects and variable interactions while remaining robust to noisy, low-volume datasets, making it well suited to experimental fermentation settings. It also provides useful insights into which variables matter most, which is crucial when working with biological systems.

In this article, I’ll show how I simulated microbial fermentation data, trained an XGBoost model, and explored how different parameters impact final microbial density.

Simulating a fermentation dataset

In microbiology, cell growth often follows a sigmoid-shaped curve. One of the most common formulations is the logistic growth equation:

N(t)=Nmax1+exp[k(ttm)]N(t) = \frac{N_{\max}}{1 + \exp\left[-k(t-t_m)\right]}

Where:

  • N(t)N(t) is microbial population, in log CFU/mL, at time tt.
  • NmaxN_{\max} is maximum population density.
  • kk is growth rate.
  • tmt_m is time when growth rate reaches its peak.

This equation produces a slow start, an exponential phase, and a plateau, just like real fermentation.

What influences growth?

I made NmaxN_{\max}, kk, and tmt_m dependent on input variables to simulate biological behavior:

VariableSimulated range
Temperature15–45°C
Sugar concentration10–100 g/L
Salt concentration0–5%
Initial pH4.5–6.5
Inoculum5–9 log CFU/mL
Fermentation time6–48 hours

To simulate natural experimental noise, I added Gaussian noise with mean 0 and standard deviation 0.2.

Dataset simulation

import numpy as np
import pandas as pd

np.random.seed(42)
n = 1000

# Input variables
temperature = np.random.uniform(15, 45, n)
sugar = np.random.uniform(10, 100, n)
salt = np.random.uniform(0, 5, n)
initial_pH = np.random.uniform(4.5, 6.5, n)
inoculum = np.random.uniform(5, 9, n)
time = np.random.uniform(6, 48, n)

# Growth parameters
k = 0.1 + 0.05 * ((temperature - 30) / 10) - 0.03 * salt
k = np.clip(k, 0.01, 0.3)
N_max = 9 + 0.01 * sugar - 0.05 * salt
N_max = np.clip(N_max, 7, 12)
t_m = 24 - 0.5 * (inoculum - 5)

# Logistic growth with noise
N_t = N_max / (1 + np.exp(-k * (time - t_m)))
N_t += np.random.normal(0, 0.2, size=n)
N_t = np.clip(N_t, 5, 12)

# Final dataset
df = pd.DataFrame({
    "temperature": temperature,
    "sugar": sugar,
    "salt": salt,
    "initial_pH": initial_pH,
    "inoculum": inoculum,
    "time": time,
    "final_density": N_t
})

print(df.head())

Dataset sample:

temperaturesugarsaltinitial_pHinoculumtimefinal_density
026.23620426.6619641.3085285.8454067.28798422.5326925.000000
143.52142958.7710851.2348946.0933638.22172925.8842985.925936
236.95981888.5651254.5312735.0009368.04064441.8909915.355006
332.95975575.9002401.2477315.7497485.61560020.2801845.000000
419.68055982.5905031.3597495.6434925.59699842.5252875.178634

Training an XGBoost model

We now train a model to predict final_density from all input variables.

from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from xgboost import XGBRegressor

X = df.drop(columns=["final_density"])
y = df["final_density"]

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42
)

model = XGBRegressor(objective="reg:squarederror", n_estimators=100)
model.fit(X_train, y_train)

y_pred = model.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print(f"Baseline RMSE: {rmse:.3f}")

Evaluation with RMSE

I use Root Mean Squared Error, or RMSE, as evaluation metric:

RMSE=1ni=1n(yiy^i)2\operatorname{RMSE} = \sqrt{\frac{1}{n}\sum_{i=1}^{n}(y_i-\hat{y}_i)^2}

If RMSE is 0.259, model predictions differ from observed values by approximately 0.259 log CFU/mL on scale measured by this error metric. This is a good result considering model simplicity and intentionally noisy data.

Feature importance

import matplotlib.pyplot as plt
from xgboost import plot_importance

plot_importance(model)
plt.title("Feature Importance")
plt.tight_layout()
plt.show()
Horizontal XGBoost feature importance plot led by temperature, time, salt, and sugar
Baseline XGBoost feature importance scores for simulated fermentation variables.

Most important variable is temperature, followed by fermentation time, salt, and sugar. This makes biological sense:

  • Temperature drives metabolic activity of microorganisms.
  • Time allows growth to reach its plateau.
  • Salt generally inhibits microbial growth.
  • Sugar provides a carbon source, although its effect saturates.

Importance scores align with microbiological principles. Temperature has highest impact on predicted outcomes, confirming its strong and known regulatory role in fermentation dynamics.

Feature importance should still be interpreted cautiously: it describes how fitted model uses variables in this simulated dataset, not universal biological causality.

Predictions versus ground truth

import matplotlib.pyplot as plt
import numpy as np
from sklearn.metrics import mean_squared_error

y_pred = model.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))

plt.figure(figsize=(6, 6))
plt.scatter(y_test, y_pred, alpha=0.5)
plt.plot([5, 12], [5, 12], "--", color="gray")
plt.xlabel("True")
plt.ylabel("Predicted")
plt.title("Predictions vs Actual Values")
plt.grid(True)
plt.tight_layout()
plt.show()
Scatter plot comparing predicted and actual microbial densities against a diagonal reference line
Baseline predictions against simulated ground-truth microbial densities.

Scatter plot shows dense clustering of predictions between 5.0 and 6.5 log CFU/mL, tightly aligned along diagonal. This indicates strong model performance in moderate growth range, where biological responses are more predictable.

For higher microbial densities above 7.5, predictions become more scattered and deviate from diagonal. Possible reasons include:

  • Increased biological variability in high-activity scenarios
  • Limits of features used
  • Greater sensitivity to noise in extreme regions

In practice, fermentations with extremely high activity can exhibit nonlinear dynamics that are difficult to capture without richer features or mechanistic modeling.

Improving the model

Feature engineering

df["sugar_per_hour"] = df["sugar"] / df["time"]
df["temp_salt_ratio"] = df["temperature"] / (df["salt"] + 1)
df["growth_potential"] = (df["temperature"] - 30) * (df["sugar"] - df["salt"])

X = df.drop(columns=["final_density"])
y = df["final_density"]

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42
)

These features combine key inputs and express domain knowledge more explicitly.

Hyperparameter tuning

model = XGBRegressor(
    objective="reg:squarederror",
    n_estimators=300,
    max_depth=4,
    learning_rate=0.05,
    subsample=0.8,
    colsample_bytree=0.8,
    reg_lambda=1.0,
    reg_alpha=0.1
)

model.fit(X_train, y_train)
y_pred = model.predict(X_test)
improved_rmse = np.sqrt(mean_squared_error(y_test, y_pred))

This configuration uses smaller learning steps, more trees, regularization, and subsampling to reduce overfitting and improve generalization.

Results after improvements

Improved RMSE: 0.2 log CFU/mL

This is approximately 23% reduction in error compared with baseline.

Updated feature importance

New engineered features such as temp_salt_ratio and sugar_per_hour now rank above temperature and sugar, indicating that domain-specific transformations better capture effects of combined variables.

Updated XGBoost feature importance plot led by time, temperature-to-salt ratio, and sugar per hour
Feature importance after adding domain-informed variables and tuning model.
Updated scatter plot comparing predicted and actual microbial densities against a diagonal reference line
Predictions after feature engineering and hyperparameter tuning.

Updated prediction plot

New scatter plot shows predictions more tightly clustered around diagonal, with reduced variance at higher densities. This indicates better generalization across broader range of simulated biological behavior.

Limits of this experiment

This workflow demonstrates modeling process on synthetic data. Reported performance measures recovery of relationships deliberately encoded in simulator; it does not validate performance on real fermentation experiments. Real applications require independent experimental data, appropriate validation design, uncertainty analysis, and careful control of biological and measurement variability.

Key takeaways

  • Machine learning can model simulated biological systems, even with noisy data.
  • Feature engineering plays a critical role in capturing complex interactions.
  • Hyperparameter tuning can produce meaningful performance gains.
  • Metrics and visualizations such as RMSE and prediction plots are essential for validation.
  • Strong performance on simulated data does not replace validation on real fermentation data.