Smoothing Out the Noise in Your Fermentation Data
Your kombucha sits in its jar, bacteria and yeast converting sweet tea into something sour and fizzy. You’ve got a pH sensor taped to the side, logging readings every minute. The problem: it bounces. 3.21, 3.38, 3.19, 3.42. The pH isn’t actually swinging that wildly—the sensor is just noisy.
You could average the last ten readings, but that feels sluggish. When the pH genuinely drops, you want to know soon, not after ten samples of lag. Enter the exponential moving average: a filter that weighs recent samples more heavily than old ones, giving you smooth trends without losing responsiveness.
The math is dead simple. Each new reading gets blended with the previous average using a smoothing factor α (between 0 and 1). Small α means heavy smoothing, large α means quick response.
fun ema(readings: List<Double>, alpha: Double): List<Double> {
val smoothed = mutableListOf<Double>()
var avg = readings.first()
readings.forEach { reading ->
avg = alpha * reading + (1 - alpha) * avg
smoothed.add(avg)
}
return smoothed
}
val raw = listOf(3.21, 3.38, 3.19, 3.42, 3.15, 3.29)
println(ema(raw, 0.3)) // [3.21, 3.26, 3.24, 3.29, 3.25, 3.26]
In Forth, which was the language of choice for embedded controllers in the 1970s, you’d handle this with stack operations. Variables hold state, and each new sample updates the running average in place:
variable avg 3.21e avg f!
variable alpha 0.3e alpha f!
: update-ema ( F: reading -- smoothed )
alpha f@ f* avg f@ 1.0e alpha f@ f- f* f+
fdup avg f! ;
3.38e update-ema f. \ 3.261
3.19e update-ema f. \ 3.2397
Pick α based on how jittery your sensor is versus how fast your process actually changes. For kombucha, where pH drops over days, 0.2 or 0.3 works well—enough smoothing to ignore sensor drift, enough speed to catch a stalled fermentation before your second batch turns to vinegar.