Skip to content

Next Bill Estimate

delabrcd edited this page Jun 7, 2026 · 2 revisions

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), never totalDueAmount. See Data Accuracy.

The short version

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.

Why the naive method isn't enough

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:

  1. A flat $/unit rate 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 $/therm explodes (the bill barely changes while usage collapses) — yet $/day actually drops. Pricing near-zero summer usage at a blended annual $/therm is wrong in both directions at once. (A degree-day-only estimate, issue #44, missed this too and was reverted.)
  2. Trailing-12 rates lag a rising market. Averaging a year of rates badly underprices the next bill when rates are climbing.

The model (issue #67)

Three independent pieces, each pure and testable:

1. Weather-normalized usage

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.)

2. Per-component fixed + variable rates

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 $/unit for that component.

The four components sum to the period's currentCharges on real bills, so summing the four predictions reconstructs the bill.

3. Trailing bias correction

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.

Confidence band

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.

Fallback

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.

How it was validated

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).

Where it lives

  • app/src/lib/prediction.tsestimateNextBillSeasonal() (this model), estimateNextBill() (the #9 fallback), the degree-day usage fits, and the per-component rate fit. All pure.
  • app/src/lib/queries.tsgetOverview() 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.

Clone this wiki locally