-
Notifications
You must be signed in to change notification settings - Fork 1
Next Bill Estimate
How the dashboard predicts your next bill before it's issued. All of this is a
forward estimate — it is never persisted and never fed to GET /api/verify (which only
cross-checks stored numbers against the bill PDFs). Pure logic lives in
app/src/lib/prediction.ts and is hand-calculated unit-tested.
Cost basis, as everywhere:
Bill.currentCharges(parsed from the PDF), nevertotalDueAmount. See Data Accuracy.
predicted bill = Σ over the 4 cost components ( fixed $/day · days + variable $/unit · usage )
+ trailing bias correction
where usage is weather-normalized (degree-days → expected kWh/therms) and the rates are learned per component from your recent bills.
The original estimate (issue #9, the "calendar" method) was: same-calendar-month-last-year usage ×
current trailing-12-month all-in $/unit. It is cheap and robust but has two structural blind
spots, both of which made it systematically under-predict on a real, rising-rate account:
-
A flat
$/unitrate ignores the fixed customer charge. Gas delivery in particular is mostly a fixed daily charge. In summer your therms fall to nearly zero, so the effective$/thermexplodes (the bill barely changes while usage collapses) — yet$/dayactually drops. Pricing near-zero summer usage at a blended annual$/thermis wrong in both directions at once. (A degree-day-only estimate, issue #44, missed this too and was reverted.) - Trailing-12 rates lag a rising market. Averaging a year of rates badly underprices the next bill when rates are climbing.
Three independent pieces, each pure and testable:
Fit usage against degree-days over each bill period (see Weather and Degree-Days), then project onto climatological-normal degree-days for the predicted next-bill window:
electric: kWh ≈ baseload_elec + slopeH·HDD + slopeC·CDD (heating + cooling)
gas: therms ≈ baseload_gas + slopeH·HDD (heating only)
This separates weather-independent baseload (fridge, lights, standby) from weather-driven load,
so a hot summer or cold snap moves the estimate the right way. The normals come from the cached
WeatherDaily history — no live network call is required for an estimate. (Reuses the same fit
machinery as the 12-month Range and Customization, issue #52.)
The four cost components — electric supply, electric delivery, gas supply, gas delivery — are priced separately. For each, a two-parameter least-squares fit over your most recent ~6 bills:
component $ ≈ fixed_per_day · days + rate_per_unit · usage
- The fixed term captures the customer/service charge, so a near-zero-usage summer gas bill is
correctly priced as "mostly the fixed charge" instead of a blown-up
$/therm. - Fitting over a recent window (not a 12-month average) tracks rising rates.
- A degenerate fit (negative fixed or rate) falls back to a simple mean
$/unitfor that component.
The four components sum to the period's currentCharges on real bills, so summing the four
predictions reconstructs the bill.
A small online correction: add the mean of the last 6 residuals, where each residual is
actual − model-prediction computed walk-forward (each historical bill predicted using only
bills before it — no leakage). This removes any small residual bias the rate/usage fits leave
behind.
low/high come from the spread (≈ ±1σ) of the recent prediction residuals, falling back to ±1σ
of recent costs, then to ±15% — so the band reflects how well the model has actually been tracking.
When there isn't enough history to fit reliably (roughly < 18 bills, or the per-component costs / degree-days are missing), the estimate falls back to the calendar (#9) method automatically. New self-hosters therefore still get a sensible number from day one; the weather-and-rate model takes over once a couple of years of bills exist.
The model was developed against a walk-forward back-test on a real multi-year account — for
each bill, train only on prior bills and compare the prediction to the actual currentCharges.
Versus the calendar method this roughly halved the error (mean absolute percentage error fell
from ~15% to ~8%) and removed the systematic under-bias. Rejected along the way: a seasonal rate
multiplier on top of the recent level (it double-counts the season and over-corrects), and
recent-years-only weather normals (no measurable effect — the residual bias was rate-trend, not
weather).
-
app/src/lib/prediction.ts—estimateNextBillSeasonal()(this model),estimateNextBill()(the #9 fallback), the degree-day usage fits, and the per-component rate fit. All pure. -
app/src/lib/queries.ts—getOverview()calls the seasonal estimate and falls back to #9. -
app/test/— hand-calculated unit tests for the rate decomposition, the full estimate, the bias correction, and the fallback path.