Estimating Customers per Transformer When Meter Data Is Missing
Every affected-customer count rests on knowing which customers each transformer serves, and no estate knows that for every transformer. Meters are added and moved, premises are subdivided, rural services are fed from transformers that were never mapped to them, and acquisitions arrive with a customer system that does not speak to the network model. The instinct is to fill the gap with an average and move on, which works right up to the point where the number appears in a reliability filing and somebody asks how it was derived. The alternative is not better data — that is a multi-year programme — but an explicit ladder of methods, each carrying its own confidence label, so a number’s provenance travels with it. This guide builds that ladder, extending the impact machinery in impact analysis and affected-customer tracing.
Environment Prerequisites
- Python 3.11 with
pandas>=2.0andgeopandas>=1.0for the spatial fallback. - A meter-to-transformer mapping wherever one exists, from the metering or customer information system, keyed on a stable identifier.
- Billing accounts joinable through a premise identifier, which is the second-best evidence and usually covers a large share of the remainder.
- Service points with geometry and a transformer association, for the modelled fallback.
- Transformer ratings and a class-average table derived from the measured population, not from a national figure, so the assumption at least reflects this estate.
- A confidence field on the output, because the whole design depends on the method travelling with the number.
Schema-Aware Validation Protocol — Run Before Estimating Anything
- Measure the coverage of each method first. Knowing that eighty-nine per cent of transformers resolve from measured data changes how much effort the remainder deserves.
- Check the meter mapping for staleness. A mapping that has not been refreshed since a subdivision programme will attribute new premises to the wrong transformer confidently.
- Confirm the premise identifier is stable across systems. A join that silently drops unmatched rows understates every count it touches.
- Derive class averages from this estate’s measured population. An average taken from an industry figure describes somebody else’s network.
- Look for transformers with implausible counts. A distribution transformer serving four hundred customers is usually a mapping defect rather than a dense apartment block, and it is worth checking before it propagates.
Minimal Reproducible Implementation
from __future__ import annotations
import logging
from dataclasses import dataclass, field
import pandas as pd
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
LOG = logging.getLogger("customer-estimate")
MEASURED, MODELLED, ASSUMED = "MEASURED", "MODELLED", "ASSUMED"
@dataclass
class Estimate:
transformer_id: str
customers: int
method: str
confidence: str
detail: str = ""
@dataclass
class EstimateRun:
estimates: list[Estimate] = field(default_factory=list)
def coverage(self) -> dict[str, float]:
total = len(self.estimates) or 1
out: dict[str, int] = {}
for est in self.estimates:
out[est.method] = out.get(est.method, 0) + 1
return {k: round(100 * v / total, 1) for k, v in out.items()}
def estimate_customers(
transformers: pd.DataFrame,
meter_map: pd.DataFrame,
billing: pd.DataFrame,
spatial: pd.DataFrame,
class_average: dict[str, float],
) -> EstimateRun:
"""Attribute a customer count to every transformer using the best evidence available.
The ladder is deliberate: a measured mapping is used where it exists, a billing join
where it does not, a spatial association after that, and a class average only as the
last resort. Every estimate carries the method that produced it, because a number
whose provenance is lost cannot be defended in a filing.
"""
run = EstimateRun()
by_meter = meter_map.groupby("transformer_id")["meter_id"].count().to_dict()
by_billing = billing.groupby("transformer_id")["account_id"].count().to_dict()
by_spatial = spatial.groupby("transformer_id")["service_point_id"].count().to_dict()
for _, row in transformers.iterrows():
tid = str(row["transformer_id"])
if tid in by_meter:
run.estimates.append(Estimate(tid, int(by_meter[tid]), "meter_mapping",
MEASURED))
continue
if tid in by_billing:
run.estimates.append(Estimate(tid, int(by_billing[tid]), "billing_join",
MEASURED))
continue
if tid in by_spatial:
run.estimates.append(Estimate(tid, int(by_spatial[tid]), "spatial_association",
MODELLED,
"service points associated to this transformer"))
continue
rating = str(row.get("rating_class", "UNKNOWN"))
avg = class_average.get(rating)
if avg is None:
run.estimates.append(Estimate(tid, 0, "none", ASSUMED,
f"no class average for rating {rating}"))
continue
run.estimates.append(Estimate(tid, int(round(avg)), "class_average", ASSUMED,
f"estate average for {rating} rating"))
LOG.info("coverage by method: %s", run.coverage())
return run
def aggregate(estimates: list[Estimate]) -> dict:
"""Total affected customers, split by the confidence of the underlying attribution."""
out = {"total": 0, "measured": 0, "modelled": 0, "assumed": 0}
for est in estimates:
out["total"] += est.customers
out[est.confidence.lower()] += est.customers
return out
The aggregate function returning a split rather than a single total is the part that matters
downstream. An impact record saying “4,120 customers, of which 3,900 measured and 220 assumed” can
be used for anything; the same record reporting only the total forces every consumer to treat all
of it as equally solid.
Improving Coverage Rather Than the Averages
It is tempting to spend effort refining the class averages, and it is almost always the wrong investment. An average is a distribution collapsed to a point, and no amount of refinement makes it a count. The effort belongs in moving transformers up the ladder.
The cheapest movement is usually the billing join, because the data exists and the obstacle is a mismatched identifier rather than missing information. The next cheapest is the spatial association, which is a modelling exercise on data already in the network. Only after both are exhausted does field verification become the sensible option, and by then it applies to a small enough population to be affordable.
Tracking the measured share over time is what makes this a programme rather than a series of one-off fixes. A share that is falling means new connections are arriving without attribution, which is a process defect at the point of connection and far cheaper to fix there than to clean up later.
Production Deployment Pattern
- Run the estimation as part of the impact pipeline, not as a separate data-preparation step, so every impact record carries current attribution.
- Publish the confidence split on every count. Dispatch can use the total; a filing uses the measured portion and states the rest.
- Recompute class averages from the measured population monthly. They drift as the estate changes, and an average derived from measured data at least tracks this network.
- Alarm on implausible counts. A transformer whose count jumps by an order of magnitude between runs is a mapping change, not a load change.
- Feed unattributed transformers into the data programme with their served load. The ones worth fixing first are the ones feeding the most customers, which the network model can rank.
- Persist the run. Coverage by method, class averages used, and the estimate per transformer, so a later filing can reproduce the number it published.
Conclusion
Missing meter data is a permanent condition rather than a temporary defect, and the useful response is an explicit ladder of methods with the confidence carried through to every consumer. Splitting a published count into measured, modelled and assumed makes it usable for dispatch and defensible in a filing at the same time. Tracking the measured share turns the underlying data gap into a programme with a visible trend, and ranking the unattributed transformers by served load is what puts the effort where the customers are.
Related
- Up to the parent topic: Impact Analysis & Affected-Customer Tracing
- Up to the section: Outage Routing & Impact Automation
- Computing Affected Customer Counts with NetworkX
- Post-Storm Reliability Reporting with Python
- Prioritizing Restoration with SAIDI/SAIFI Scoring
For authoritative reference, consult the pandas grouping guide and the IEEE 1366 reliability indices standard.