BillConverter

A MeterID resource

Guides 5

Commodities Electric · Gas · Water · Sewer · Steam

What Data to Extract From a Utility Bill: A Field-by-Field Schema

A field-by-field schema for utility bill extraction: identity, dates, usage, money, and derived values, with snake_case names, types, and unit handling.

Forty fields, in five groups: identity (who and where), dates (which window), usage (how much and in what unit), money (split by charge type, not just the total), and derived values (computed by you, never read off the bill). If you extract only account number, total amount due, and usage, you have a payment record. You do not have data you can analyze.

The schema below is what a workable minimum looks like. Copy it.

Grain: what one row represents

Decide this before anything else, because it determines whether the rest of the schema holds together.

One row = one meter, one commodity, one service period, on one bill.

A single Con Edison invoice can carry electric and gas for the same address. A single National Grid commercial account can cover four meters. A PG&E bill can bill one service agreement across two rate periods after a mid-cycle tariff change. Flatten all of that into one row per PDF and you will be re-deriving the split forever. Keep a bill_id on every row so you can still reconcile the row set back to the invoice total.

Identity

These fields answer “which physical thing at which address does this consumption belong to.” People conflate them constantly, and the conflation is what breaks portfolio-level reporting.

The account number is a billing relationship. It can cover twelve buildings. It changes when ownership changes, and it does not change when the meter is swapped.

The meter number is a serial number on a physical device. It changes when the utility replaces the meter, which happens without warning and mid-portfolio during AMI rollouts. Trending usage keyed on meter number produces a phantom site every time a meter gets swapped.

The point of delivery ID goes by a different name at nearly every utility: PG&E calls it a Service Agreement ID, SCE issues a 10-digit Service Account Number distinct from its 12-digit Customer Account Number, ERCOT territory uses an ESI ID, and PJM-area utilities use POD ID. It is the stable one. It identifies the delivery point regardless of who owns the building or which meter is bolted to the wall. If a utility prints one, store it, and key your time series on it. The full breakdown is in account number vs meter number vs service ID.

Premise ID is separate again. Xcel Energy uses a 9-digit Premise Number, which appears on page 2 of the bill under Electricity Service Details rather than up front with the account number.

Rate schedule is the identity field most people skip, and it is the one that explains a cost jump when usage did not move. Capture it exactly as printed, because the naming conventions do not rhyme: PG&E writes B-19 and B-10S, SCE writes TOU-GS-3, Georgia Power writes TOU-GSD-17, National Grid’s Massachusetts business tariff is G-2, and Con Edison uses a space rather than a hyphen in SC 2 and SC 9 Rate I. Normalizing all of those into one house style feels tidy and destroys your ability to match a code back to a published tariff sheet.

Dates

Four date concepts, routinely mislabeled:

  • Service period start and end. The window the consumption actually happened in. Printed as “Service Period”, “Billing Period”, “Service From/To”, or just “From / To” depending on the utility.
  • Read dates. When the meter was physically read. Usually identical to the service period, sometimes off by a day or two, and on estimated reads they are fiction.
  • Bill date (also “Statement Date”, “Invoice Date”). When the utility generated the document.
  • Due date. Accounts payable cares. Analysis does not.

Service period is the only one that belongs on the x-axis of anything. Bill date is what people use instead, because it is easier to find and always present, and it is wrong: a bill dated April 3 covers March. Assign that usage to April and every heating season shifts by a month. If you are pushing data into ENERGY STAR Portfolio Manager, it rejects the bill date entirely and wants start and end.

Store dates as ISO YYYY-MM-DD. US utilities print MM/DD/YYYY, occasionally 03/14/25, and Con Edison writes month names out. Normalize on ingest.

Usage

Consumption plus its unit, and the unit is not optional metadata. A bare number is unusable.

Electric is easy: kWh, sometimes with demand in kW alongside it.

Gas is not. CCF is one hundred cubic feet. MCF is one thousand cubic feet. A therm is 100,000 BTU, which is a heat unit, not a volume unit, so converting between them requires a heat-content factor that the utility prints on the bill and that changes with gas composition. It generally sits a little above 1.0 therm per CCF. Some bills print CCF and therms both; DTE bills gas in Ccf and Mcf; National Grid in Massachusetts bills in therms directly. Do not hardcode 1.0.

Water is worse, because the same physical unit has two abbreviations. CCF and HCF both mean one hundred cubic feet, which is 748 gallons. LADWP prints HCF, Seattle Public Utilities prints CCF, and plenty of Texas and southeastern utilities bill in kGal (one thousand gallons). Three different labels, three different magnitudes, one column.

The rule that follows: never store a quantity without storing its unit in the same row, and never convert on ingest. Convert in the query layer where the conversion is visible and reversible.

Read type matters more than its size suggests. Utilities mark reads Actual or Estimated, and an estimated read followed by a true-up produces one artificially flat month and one artificially huge one. Both are real charges. Neither is real consumption for that period. Without a read_type flag you cannot tell that pattern from an actual operational change.

For commercial electric, also capture demand (kW), and whether that demand figure is measured or billed. Many industrial tariffs apply a ratchet where billed demand is a percentage of the highest demand set in the preceding eleven months, so billed demand can exceed anything that happened this month. Power factor belongs here too, since tariffs that penalize below 0.90 or 0.95 make it a cost driver rather than a curiosity.

Money

Total amount due is the number everyone extracts and the least analytically useful one on the page. It contains prior balance, late fees, and whatever adjustment the utility applied for a billing error six months ago. It is a remittance figure.

What you need is the split. In a deregulated market, the bill separates supply (also “generation”, “energy”, “basic service”) from delivery (also “distribution”, “transmission”, “delivery services”). National Grid prints them as two clearly labeled sections. PG&E splits generation from delivery and then adds PCIA and franchise fee surcharges on the delivery side. Under a community choice aggregator, the generation charge belongs to a third party and appears as a pass-through line, or arrives as a separate invoice entirely.

The split is the whole analysis. Supply is the part you can shop or contract for. Delivery is the regulated part you cannot. A blended $/kWh that moved from $0.19 to $0.22 tells you nothing; supply moving from $0.11 to $0.14 while delivery held flat tells you your contract rolled to a variable rate. Total-only extraction throws that away permanently, and you cannot recover it later without re-processing every PDF.

Capture taxes, fees, and late charges separately from both. Credits and adjustments should be a signed field, negative for credits. Bills print credits as $45.20-, (45.20), or 45.20 CR depending on the vendor’s billing system, and all three need to land as -45.20.

Derived fields: compute, do not extract

Bills print an average daily usage and sometimes a cost per kWh. Do not extract them.

The utility rounded those to two or three digits for display, and the rounding is not consistent across vendors or across a vendor’s own bill formats. Pull the printed figure into your data and you have imported someone else’s rounding error into a number you will later multiply by twelve months and a portfolio of sites. Compute from the fields you already have:

days_in_period    = service_period_end - service_period_start
average_daily_usage = usage_quantity / days_in_period
blended_rate      = current_charges / usage_quantity
load_factor       = usage_quantity / (demand_quantity * days_in_period * 24)

Pick a days_in_period convention and write it down. End minus start gives 31 for a March 14 to April 14 period, which matches what most utilities print as billing days. Inclusive counting gives 32 and will quietly disagree with the bill forever.

The schema

FieldTypeUnit handlingReqNotes
bill_idstringn/arequiredYour key, one per source document
utility_namestringn/arequiredBiller as printed, normalized to a controlled list
account_numberstringn/arequiredString, never integer; preserves leading zeros and dashes
invoice_numberstringn/aoptionalAbsent on many residential formats
service_addressstringn/arequiredKeep unparsed; parse to components separately if needed
premise_idstringn/aoptional9-digit Xcel “Premise Number”, page 2
point_of_delivery_idstringn/aoptionalService Agreement ID, ESI ID, POD. Key time series here
meter_numberstringn/aconditionalRequired at meter grain. Changes on meter swap
rate_schedulestringn/aoptionalTariff code exactly as printed: B-19, TOU-GSD-17, SC 9 Rate I
commodityenumn/arequiredelectric, natural_gas, water, sewer, steam
service_period_startdateISO 8601requiredThe analysis date
service_period_enddateISO 8601required
read_date_previousdateISO 8601optionalMay differ from service period
read_date_currentdateISO 8601optional
bill_datedateISO 8601optionalStatement date. Not for analysis
due_datedateISO 8601optionalAP only
usage_quantitydecimal(14,3)in usage_unitrequiredNever store without the unit
usage_unitenumcanonicalrequiredkWh, therms, CCF, MCF, gal, kGal, Mlb
usage_unit_rawstringas printedoptionalHCF, Ccf, 100 cu ft
read_typeenumn/aoptionalactual, estimated, customer, unknown
demand_quantitydecimal(12,3)in demand_unitoptionalCommercial and industrial electric
demand_unitenumn/aoptionalkW, kVA
demand_basisenumn/aoptionalactual, billed, ratchet
power_factordecimal(5,4)dimensionlessoptional0–1. Penalty threshold is tariff-specific
currencystring(3)ISO 4217requiredUSD
total_amount_duedecimal(12,2)currencyrequiredRemittance figure, not a cost metric
current_chargesdecimal(12,2)currencyrequiredThis period only
prior_balancedecimal(12,2)currencyoptional
supply_chargesdecimal(12,2)currencyoptionalGeneration, energy, basic service
delivery_chargesdecimal(12,2)currencyoptionalDistribution, transmission
taxesdecimal(12,2)currencyoptional
feesdecimal(12,2)currencyoptionalFranchise, regulatory, surcharges
late_chargesdecimal(12,2)currencyoptional
credits_adjustmentsdecimal(12,2)currencyoptionalSigned; negative for credits
days_in_periodintegerdayscomputedend - start
average_daily_usagedecimal(14,4)usage_unit/daycomputed
blended_ratedecimal(12,6)currency/usage_unitcomputedFrom current_charges
load_factordecimal(5,4)dimensionlesscomputedRequires demand
source_file_namestringn/arequiredProvenance
source_pageintegern/aoptionalMulti-account PDFs

Worked example

A National Grid commercial electric bill, one meter, March 14 to April 14, 2025:

{
  "bill_id": "ng-2025-04-000418",
  "utility_name": "National Grid",
  "account_number": "0231-4408-712",
  "invoice_number": "88420176",
  "service_address": "412 Bridge St, Lowell, MA 01852",
  "point_of_delivery_id": "51004402281",
  "meter_number": "0084213556",
  "rate_schedule": "G-2",
  "commodity": "electric",
  "service_period_start": "2025-03-14",
  "service_period_end": "2025-04-14",
  "read_date_previous": "2025-03-14",
  "read_date_current": "2025-04-14",
  "bill_date": "2025-04-17",
  "due_date": "2025-05-08",
  "usage_quantity": 24880.0,
  "usage_unit": "kWh",
  "usage_unit_raw": "KWH",
  "read_type": "actual",
  "demand_quantity": 78.4,
  "demand_unit": "kW",
  "demand_basis": "billed",
  "power_factor": null,
  "currency": "USD",
  "total_amount_due": 5472.14,
  "current_charges": 5472.14,
  "prior_balance": 0.0,
  "supply_charges": 3528.98,
  "delivery_charges": 1943.16,
  "taxes": 0.0,
  "fees": 0.0,
  "late_charges": 0.0,
  "credits_adjustments": 0.0,
  "days_in_period": 31,
  "average_daily_usage": 802.5806,
  "blended_rate": 0.219941,
  "load_factor": 0.4265,
  "source_file_name": "lowell-bridge-st-2025-04.pdf",
  "source_page": 1
}

Supply plus delivery equals current charges equals total due, since prior balance is zero. Load factor is 24,880 divided by (78.4 × 31 × 24), or 42.7%, which is a normal-looking profile for a light manufacturing site. That last number is the one that tells you whether a demand-response conversation is worth having, and it exists only because demand was captured.

Keep the raw string

For anything ambiguous, store the parsed value and the literal text next to it. usage_unit_raw, account_number_raw, rate_schedule_raw, and a raw date string are the ones that earn their keep.

The reason is that parse failures are silent. If a water bill prints “HCF” and your normalizer maps it to CCF, that is correct, and you want the audit trail. If it prints “units” and your normalizer guesses CCF, that is a coin flip, and six months later when the numbers look off by a factor of 7.48 the raw string is the only thing that lets you find out what happened without re-opening 400 PDFs. Same for 45.20 CR versus (45.20) versus 45.20-. Same for a rate code that got OCR’d as B-l9.

This matters more with model-based extraction than with template parsing, because a model will confidently return a plausible unit rather than failing loudly. That failure mode and several others are covered in why AI utility bill extraction fails. Raw strings are the cheapest defense: a few text columns against having to reprocess your archive.

Fields not worth extracting

The 13-month usage history bar chart. Every major utility prints one, and people try to OCR it because it looks like free historical data. It is rendered as a graphic with no numeric labels on most formats, the axis scale is unstated, and the values are the utility’s own restated history, which does not always match what the individual bills said after adjustments. You will spend more effort on the chart than on the rest of the bill and get numbers you cannot reconcile. Get history by processing the actual bills.

Also skip “Amount Enclosed” from the remittance stub. It is blank or a duplicate of total due. And skip the printed average daily usage, for the rounding reason above.

Prior payments received is a judgment call. If you are reconciling AP, capture it. If you are analyzing energy, it is noise.

Doing this every month?

At some point the manual version stops being worth it. MeterID collects, validates, and delivers utility bill data for multi-site portfolios — see how it works.