How your next cycle is estimated
A look under the hood — every prediction is built from your own history, on your device, with robust statistics and an honest measure of uncertainty.
The one-sentence version
FlowKeeper predicts your next period as your last start date + a recency-weighted, outlier-cleaned average of your own cycle lengths — then wraps that single date in an 80% confidence band sized to how regular you actually are.
Where Slast is the day your current cycle began, Lk are your cleaned past cycle lengths (k = cycles ago, 0 = most recent), and Δ is the uncertainty half-width derived below. Everything else is just how we choose and clean the Lk.
It only ever uses your data
No population averages, no machine-learning model trained on other people, nothing leaves the phone.
Each completed cycle is reduced to two numbers: the date it started and how many days it
lasted. That's the entire input — a chronological list of
(startDate, cycleLength).
Why so minimal? For calendar-only tracking, the accuracy ceiling is set by your own
biological variability, not by model cleverness — mean, median, and ML point-predictors
land within a day of each other. So the engine spends its effort where it matters:
robustness to messy logs and honesty about uncertainty.
Clean the history — twice
A single weird cycle shouldn't move your forecast. Two filters remove the noise before any math runs.
Filter A — physiological range. Anything outside 21–45 days is treated as a mis-log or a one-off, not a cycle, and dropped. Then the window narrows to the 13 most recent survivors (≈ a year), so the math always reflects roughly the last year.
Filter B — statistical outlier. Of those, FlowKeeper computes the median and the MAD (median absolute deviation), then rejects any length whose modified z-score is too large. MAD is used instead of standard deviation precisely because one extreme value barely budges it.
Finally, in auto mode, if you're highly variable (MAD > 4 days) or clearly trending, the engine narrows further to just your last 4 cycles so the estimate reflects your current state.
Weight recent cycles more
Last month tells us more about next month than a cycle from a year ago.
Rather than a plain average, each cycle's influence decays by a factor of 0.85 for every cycle further into the past. The result is a normalized recency-weighted mean — responsive to gradual change (postpartum, perimenopause, lifestyle) without overreacting to any single month.
Don't pretend to know the exact day
A single date is a comforting lie. FlowKeeper shows a range — and sizes it to you.
The half-width Δ is an 80% prediction interval built from your own dispersion. The MAD is scaled into a robust standard deviation, multiplied by a Student-t factor (small samples → wider), and inflated by √(1+1/n) because we're predicting one new cycle, not the long-run average.
Translate spread into plain words
The same MAD that sizes the band also picks a confidence label you actually see.
Separately, if the spread between your shortest and longest healthy cycle exceeds 10 days, FlowKeeper flags your cycles as irregular — a cue, not a diagnosis.
Ovulation & fertile window come for free
Once the next start is known, two fixed offsets place the rest of the calendar.
These are coarse, population-standard offsets (a predicted period is also drawn as 5 days for calendar shading) — deliberately simple, and clearly framed as estimates rather than measured ovulation.
Worked example, end to end
Five logged cycles, one of them anomalous — watch the pipeline absorb it.
Input
Cleaned cycle lengths, oldest → newest: [28, 27, 29, 28, 40] days. Current cycle began Mon, Jun 8, 2026. Window: auto.
- ① Range filterall of 28,27,29,28,40 ∈ 21–45 → all kept
- ② Outlier filtermedian 28, MAD 1 → z(40)=0.6745·12 = 8.1 › 3.5, drop
40→ [28,27,29,28] - ③ Recency meanL̂ = (28·.61 + 27·.72 + 29·.85 + 28·1) / 3.18 = 28.04 → round 28
- ④ Interval Δt₃·(1.4826·0.5)·√1.25 = 1.638·0.7413·1.118 ≈ 1.36 → ±1 day
- ⑤ ConfidenceMAD 0.5 ≤ 1.5 and n = 4 ≥ 3 → HIGH
…and for free: ovulation ≈ Jun 22 (Jul 6 − 14), fertile window opens Jun 17 (Jun 22 − 5).
So how accurate is it?
Honest answer: as accurate as your body is regular — and the design leans into that, instead of hiding it.
For calendar-only prediction, the hard ceiling is your own cycle variability. No amount of modeling beats it, which is why fancier predictors gain almost nothing over a well-cleaned average. FlowKeeper's accuracy story is therefore three concrete guarantees, not a magic number:
And where it's not precise, it says so — low-confidence labels, an irregularity flag, and a visible range instead of a false single date. The goal isn't to predict the unpredictable; it's to be right about how sure it is.
The tuning constants
Every load-bearing number, in one place — byte-identical across platforms.
| Constant | Value | Role |
|---|---|---|
| cycleLengthDays | 28 | Fallback when there's no history |
| healthyCycleRange | 21–45 | Physiological range filter |
| recencyDecay | 0.85 | Per-cycle weight decay |
| outlierZThreshold | 3.5 | Modified-z rejection cutoff |
| outlierScaleFactor | 0.6745 | Iglewicz–Hoaglin constant |
| madToSigmaFactor | 1.4826 | MAD → robust σ |
| maxWindowCycles | 13 | History cap (≈ 1 year) |
| adaptiveRecentCycles | 4 | Fallback window if variable/trending |
| minCyclesForInterval | 3 | Below this → cold-start band |
| coldStartBandDays | ±4 | Default band with little history |
| periodLengthDays | 5 | Length drawn for a predicted period |
| highConfidenceMAD / mediumConfidenceMAD | 1.5 / 4.0 | Confidence label cutoffs |
| ovulationOffsetDays / fertileWindowLeadDays | 14 / 5 | Derived-calendar offsets |
Source of truth: shared/…/cycle/CycleEngine.kt &
cycle-engine-golden-vectors.json. On-device · local-only · no network.