Honest Statistics Instead of "AI Magic": How I Built a Forecast Engine Without Neural Networks#
Every other App Store listing today promises "AI-powered insights." You open the app and find either a wrapper around an external LLM or a black box that spits out "risk: high" without a single word of explanation. When I built MeteoHealth — an app that looks for connections between weather, sleep, activity, and wellbeing — I deliberately took a different path. There's no neural network inside, no Core ML, just classical statistics: Pearson correlations, analysis of variance with p-values, and a Benjamini–Hochberg correction for multiple comparisons. Everything runs on-device, offline, and — this is the part I care about most — every output can be explained in one plain sentence. This is how that engine works, and why I think honest statistics is a stronger product decision than a trendy "AI-powered" label.
The "AI-Powered" Hype, and Why It Made Me Cautious#
The problem isn't that neural networks are bad — for image recognition or text generation they're irreplaceable. The problem is that "AI" became a marketing word slapped on anything, including plain if-else rules. In health tech this is especially dangerous: a user sees "78% risk of a flare-up" and has no way to ask the app "why." Regulators noticed too — the EU AI Act, for instance, classifies medical AI systems by risk tier, and starting August 2026 it requires high-risk systems to provide explanations that let the people relying on them actually understand the output. Academic literature calls this the "black box problem": a model predicts but doesn't explain its reasoning, and post-hoc explanation techniques (LIME, SHAP, and similar) only approximate the real logic rather than reproduce it — critics have argued this can mask the trust problem instead of solving it.
For a wellbeing app, that's doubly unacceptable. If MeteoHealth tells someone their headache risk is rising, they should understand it's because barometric pressure dropped while their sleep also got shorter — not because "the model decided so." That's the decision behind the engine: build it on statistics where explainability is baked into the math itself, not bolted on afterward as a separate module.
Pearson Correlation: The First Tool in the Engine#
The first and simplest tool in the engine is the Pearson correlation coefficient. It measures how linearly two variables move together — say, barometric pressure and a daily wellbeing score. The value ranges from −1 to 1: zero means no relationship, and a value close to either extreme means a strong one.
/// Pearson correlation coefficient between two data series
func pearsonCorrelation(_ x: [Double], _ y: [Double]) -> Double? {
guard x.count == y.count, x.count > 1 else { return nil }
let n = Double(x.count)
let meanX = x.reduce(0, +) / n
let meanY = y.reduce(0, +) / n
var numerator = 0.0
var sumSqX = 0.0
var sumSqY = 0.0
for i in 0..<x.count {
let dx = x[i] - meanX
let dy = y[i] - meanY
numerator += dx * dy
sumSqX += dx * dx
sumSqY += dy * dy
}
let denominator = (sumSqX * sumSqY).squareRoot()
guard denominator != 0 else { return nil }
return numerator / denominator
}Nothing magical here — sums, means, a square root. But this plain code has one property that matters a lot: I can open it in a debugger, feed in a real user's data, and see exactly the same numbers the engine sees. No neural network offers that kind of transparency — even a small fully connected network with a couple of hidden layers already has thousands of weights, and there's no way to extract a human-readable "why" from them.
Analysis of Variance, P-Values, and the Multiple Comparisons Problem#
A correlation alone isn't enough — you also need to know how much to trust it given the amount of data you actually have. That's where analysis of variance (ANOVA) and p-values come in: they answer the question "could this result have happened by chance if there's actually no relationship at all?" The smaller the p-value, the less likely the pattern is just noise.
But there's a classic statistical trap hiding here. MeteoHealth checks dozens of "factor vs. wellbeing" pairs at once — pressure, humidity, sleep, steps, caffeine, HRV, and more. If you test each pair separately at a p < 0.05 threshold, then out of 20 tests, one will look "significant" purely by chance on average — that's the multiple comparisons problem. Without correcting for it, the app would start surfacing patterns to users that look convincing but don't hold up on new data.
Imagine a user who had cloudy weather and a headache three days in a row. In isolation, that correlation might clear the p < 0.05 bar — but once you account for fifteen other factors tested alongside it, it's almost certainly coincidence. That's why a single p-value isn't enough — you need a step that looks at the whole picture at once, not one pair in isolation.
The Benjamini–Hochberg Correction: Filtering Out Coincidences#
The Benjamini–Hochberg correction controls the false discovery rate among all "significant" results when many hypotheses are tested at once. The idea is simple: sort the p-values in ascending order and find the largest one that still fits under a scaled threshold.
/// Benjamini–Hochberg correction for multiple comparisons.
/// Returns the indices of hypotheses that remain significant
/// after controlling the false discovery rate at `alpha`.
func benjaminiHochberg(pValues: [Double], alpha: Double = 0.05) -> Set<Int> {
let m = Double(pValues.count)
let indexed = pValues.enumerated().sorted { $0.element < $1.element }
var lastSignificantRank = -1
for (rank, item) in indexed.enumerated() {
let k = Double(rank + 1)
let threshold = (k / m) * alpha
if item.element <= threshold {
lastSignificantRank = rank
}
}
guard lastSignificantRank >= 0 else { return [] }
return Set(indexed.prefix(lastSignificantRank + 1).map { $0.offset })
}In practice: if the engine tests wellbeing against 15 factors and three come back with p < 0.05 on their own, after the correction only one might survive — the one that's actually robust. That's the one shown to the user, phrased as something like "you have a statistically significant link between humidity above 80% and lower energy" — a claim that has actually survived a check against random chance.
Personal Baselines and Adapting After ~10 Entries#
Raw values are meaningless without context: a resting heart rate of 68 is normal for one person and already a deviation for another. So the engine builds a personal baseline for every metric and updates it as new entries come in.
/// Updates a personal baseline using an exponentially weighted moving average,
/// so the model adapts as new daily entries arrive.
struct PersonalBaseline {
private(set) var mean: Double
private(set) var sampleCount: Int
private let smoothing: Double
init(initialMean: Double = 0, smoothing: Double = 0.2) {
self.mean = initialMean
self.sampleCount = 0
self.smoothing = smoothing
}
mutating func update(with value: Double) {
sampleCount += 1
if sampleCount <= 10 {
// Cold start: simple running average for the first ~10 entries
mean = mean + (value - mean) / Double(sampleCount)
} else {
// Warmed up: exponentially weighted average reacts to recent trends
mean = mean + smoothing * (value - mean)
}
}
}For roughly the first ten entries, the engine uses a plain running average — a "cold start" phase where data is scarce and sudden swings are better smoothed out evenly. After that it switches to an exponentially weighted average, which reacts a bit more to recent entries while staying resistant to one-off outliers. These personal baselines are exactly what the 24-hour and 72-hour wellbeing forecasts are built on: the engine compares current conditions — weather, sleep, activity — against a person's own normal range and against the correlations found to be significant for them specifically, rather than against averaged data pooled across every user.
Statistics vs. Neural Networks: An Honest Comparison#
I could have trained a small model on aggregated data, or wired up a cloud LLM for "smart insights." I deliberately didn't — here's why.
| Criterion | Statistical engine | Neural network / cloud AI |
|---|---|---|
| Explainability | Every output reduces to a formula and a p-value | Often a black box; needs post-hoc explanations |
| Privacy | Data never leaves the device; analysis runs offline | Usually requires sending data to a server |
| Data needed to start | Works after roughly 10 entries per person | Typically needs thousands of labeled examples |
| Works offline | Yes, always | Usually not, without a bundled model |
| Predictability | Deterministic, reproducible | Can vary across runs and model versions |
A second comparison, by task type:
| Task | Right tool for the job |
|---|---|
| Find a linear relationship between two metrics | Pearson correlation |
| Recognize an object in a photo | Neural network (Core ML, Vision) |
| Check whether a finding is statistically significant | ANOVA + p-value |
| Generate natural-language text | LLM |
| Filter out coincidences among dozens of hypotheses | Benjamini–Hochberg correction |
These tables make the point: statistics and neural networks solve different classes of problems, and one doesn't replace the other. But for this specific job — finding personal, explainable, private patterns in one person's health data — classical statistics beat neural networks on every practical criterion that mattered: it works on small samples, needs no server, doesn't hallucinate, and lets me, as the developer, show the user the exact reason a forecast turned worrying. Core ML would be a great fit for recognizing emotion in a photo or classifying activity from accelerometer data — just not for the job of explaining to a person why they might feel worse tomorrow.
This entire engine runs inside MeteoHealth, which is why every forecast there ships with an explanation, not just a number. The "AI-powered" hype will fade, but user trust is built on whether an app can explain why it said what it said. Sometimes the most honest answer to "where's the AI" is: "there isn't one — there's statistics, and it works."
I think health tech is going through the same phase web development went through with JavaScript frameworks: first everyone wants the trendiest tool, then everyone wants the tool that actually solves the problem. Classical statistics is less exciting than "a neural network predicts your health," but it's reproducible, explainable, and doesn't ask the user to trust a black box. For an app dealing with one person's wellbeing, that isn't a compromise — it's the only sensible choice.



