Methods & Formulas
How UnitTrack computes what it publishes — every disclosure-control rule and every analysis, with the formula as implemented in code, its primary citation, and an honest note wherever the implementation simplifies the textbook method.
This is the companion to the Security & PII document. That one covers how your data is protected; this one covers how it is analyzed. Both follow the same rule: claims match the code. Where UnitTrack does less than the literature (e.g. a heuristic instead of an optimal solver), we say so here rather than imply otherwise.
How to read this. Each method gives: what it answers · the formula as coded · the default parameters · the primary source · and, where relevant, a Simplified note. File references point at the exact function so an auditor can check us.
1. The unit of analysis: a "cell"
Every published number is a cell — one group of customers and the value
computed for them. Cells are the object disclosure control operates on, so their
fields matter (udap_engine/aggregate/__init__.py, class Cell):
| Field | Meaning |
|---|---|
keys |
the group — e.g. {rate_class: "RESIDENTIAL"} or {period: "2023-01"} |
value |
the published statistic (see below) |
contributor_count |
number of distinct contributors (customers/accounts) in the group |
row_count |
number of meter reads in the group |
top1_share |
the largest single contributor's share of the group total |
top2_share |
the two largest contributors' combined share of the group total |
How a cell's value is computed (_metric_value): the group total
T = Σ value over its reads, then
count→row_countmean→T / row_countsum→T
The contributor shares — the inputs to the dominance rule — are, for a group
whose per-contributor totals (each customer's summed consumption) are
c₁ ≥ c₂ ≥ … ≥ cₘ with total T:
top1_share = c₁ / T
top2_share = (c₁ + c₂) / T
Rows missing the value field are skipped; a zero-total group yields shares of 0. Cells are sorted by descending value.
Grouping options (GROUPINGS): customer class, geography, service type, or a
time grain (month / quarter / year). Geography is a real Census tract when a
boundary layer covers the data, else a fallback grid (§4).
2. Statistical disclosure control (SDC)
The core of the product. Published aggregates pass a composed pipeline —
primary threshold → dominance → complementary suppression — then a residual
re-identification risk score. All of this is pure Python operating on Cell
objects (udap_engine/sdc/__init__.py, apply_policy), with each suppression
tagged by the rule and citation that fired it (RULE_CITATIONS) so the UI can
show why a group was withheld, not just that it was.
2.1 Minimum-count threshold (k-anonymity)
A cell is suppressed if it has fewer than k distinct contributors
(_primary_threshold):
suppress cell if contributor_count < k
Default: k = 15. Rationale: a published group of n distinct
contributors bounds the chance of singling any individual out at 1/n; requiring
n ≥ k caps that exposure. Note the count is of contributors, not reads — 15
meter reads from one customer is still one contributor and does not pass.
Primary source: Sweeney, k-anonymity (2002); ESSnet/Eurostat SDC Handbook (Hundepool et al.) — the minimum-frequency / threshold rule for tabular data. Regulatory match: CA CPUC D.14-05-016 ("15/15"); IL 220 ILCS 33.
2.2 (n,k)-dominance
Even with enough contributors, a group dominated by one or two large customers
leaks: an insider who knows the total and the runners-up can back out the leader.
So a cell is suppressed when its top-n contributors hold at least a p share of
the total (_dominance):
suppress cell if topN_share ≥ p (N = dominance_n ∈ {1, 2})
with topN_share = top1_share when n = 1, else top2_share.
Default: the safe-by-default governance policy (§2.7) governs every analysis —
the single-customer 15/15 form, n = 1, p = 0.15. (n = 2, p = 0.85
exist only as the bare engine/AnalysisSpec fallback and are overridden by the
governance policy before any analysis runs, so the workbench never publishes at
the looser cap by default.) The comparison is ≥ (not >) so a cell sitting
exactly on the limit is suppressed — matching regulator language of "p or
more."
Primary source: ESSnet/Eurostat SDC Handbook §4.2 (sensitive cells in magnitude tables — the (n,k)-dominance / concentration rule). Chapter 5 covers frequency tables, so the magnitude dominance rule lives in Chapter 4.
2.3 Complementary (secondary) suppression
If a row of cells has exactly one primary hole, its value is recoverable as
margin_total − (other published cells). Complementary suppression hides a second
cell to break that subtraction (_complementary):
For each free dimension, group the cells into margins (cells sharing every key except that dimension). In any margin with exactly one suppressed cell and at least two still-published cells, suppress the smallest-valued published cell as cover. Repeat to a fixed point.
for each margin with |suppressed| == 1 and |published| ≥ 2:
hide argmin_value(published cells)
The guard requires at least two published cells present before it acts, so at least one always survives — we never blank out the last remaining published cell (that would destroy the data rather than protect it), and this view publishes no explicit totals.
Primary source: ESSnet/Eurostat SDC Handbook §4.2.2 (complementary/secondary suppression for magnitude tabular data).
Simplified — honest note. This is a smallest-value heuristic, not the Handbook's optimal secondary suppression, which is a linear program that minimizes information loss. UnitTrack does not yet solve that LP; the heuristic protects the margin but may suppress more (or less efficiently) than the optimum. It also handles one-dimensional margins per free key rather than the full multi-table network. See §5.
Heuristic limit — the 2-cell margin (L5). Complementary suppression fires only when a margin has ≥ 3 cells (one suppressed, ≥ 2 still publishable): with exactly two cells it declines, because hiding the second would destroy the row rather than protect it, and this view publishes no explicit margin totals to difference against. The residual exposure is therefore an external total: if an analyst independently knows the row total (e.g. from a separately published figure), a single published cell in a 2-cell margin reveals its partner by subtraction. UnitTrack does not — and cannot in general — reason about totals published outside the tool; the session-global differencing risk (§2.4) surfaces cross-release exposure within a session, and users combining a UnitTrack release with an outside total should treat the pair under the same minimum-count rule. This is a documented boundary of the heuristic, not a defect in it.
2.4 Residual re-identification risk score
After suppression, the published set still carries some risk — bounded by its
smallest surviving group (_score):
risk = 100 / min_group (min_group = smallest contributor_count among published cells)
risk = 0 if nothing is published
risk_score = round(risk) reported 0–100
This is a worst-case measure, not an average: exposure is set by the single
most vulnerable published group, the same logic behind minimum-count rules. Bands
(_band): ≥ 15 → Low, 5–14 → Moderate, < 5 → High, nothing
published → None — anchored to the 15/15 safe floor (1/15 ≈ 6.7% singling-out).
Primary source: Sweeney (2002); the smallest-group worst case underlies CPUC-style minimum-count rules.
Session-global risk (differencing). Risk is a property of the whole release
set, not one table: publishing several overlapping breakdowns in a session lets
someone difference them to recover a value that any single table's suppression
protected. The workbench therefore keeps a per-session ledger of the group-by
tables you publish (deduped by dimension, keeping the worst case per breakdown)
and shows a session-global risk — 100 / (smallest published group across all
of them) — beside the per-analysis score, flagging when two or more distinct
breakdowns have been published. Honest scope: this surfaces the differencing
setup and the worst-case exposure across releases; it does not attempt full
differencing detection (intractable in general) — it is a warning, not a proof.
reset_policy clears the session ledger.
2.5 Data retained (the utility side of the trade-off)
So the risk number is never read in isolation, every result also reports how much
data survived (_score):
pct_records_published = 100 · Σ row_count(published) / Σ row_count(all)
pct_value_published = 100 · Σ |value|(published) / Σ |value|(all)
Risk and retained data are surfaced together — the explicit risk/utility trade-off.
2.5a Governing-standard citation (release level)
Beyond the per-cell suppression citations (§2.1–2.3), every release names the
standard its thresholds implement, so an exported table can state the rule the
whole release follows (policy_basis, carried on the result as policy_basis and
stamped as a # Basis — line on CSV exports). When the settings match the
utility-sector 15/15 rule (≥ 15 contributors and a single customer capped at
≤ 15%), the citation names CA CPUC D.14-05-016; otherwise it reports the generic
minimum-frequency + (n,k)-dominance basis with the applied thresholds. This keeps
the citability mandate true at the artifact level, not only in the UI.
2.5b Quasi-identifiers and geographic granularity (L8)
City, ZIP, and Census tract are quasi-identifiers. Even when a value is aggregated and passes the minimum-count and dominance rules, a fine geographic key combined with other released or externally-known attributes (rate class, a service type, an approximate usage band) can narrow a group toward a single household — Sweeney's classic result that ZIP + birthdate + sex re-identifies most of the US population is the canonical warning. UnitTrack applies the same k / dominance rules to geographic groupings as to any other dimension, so a sparsely-populated tract is suppressed like any small group; but users should treat finer geography as higher risk, prefer the coarsest geography that answers the question, and remember that publishing several geographic breakdowns compounds exposure (the session-global differencing risk, §2.4, tracks this within a session). This is guidance, not an automated control — geography is a lever the analyst chooses.
2.6 Differential privacy (alternative method)
As an alternative to suppression, a group's count can be released with
calibrated noise instead of being hidden (udap_engine/dp/__init__.py,
dp_counts). For a count query one person changes the answer by at most 1, so
global sensitivity Δ = 1, and ε-differential privacy is achieved by adding
Laplace noise at scale b = Δ/ε:
noisy = max(0, round( true + Laplace(0, 1/ε) ))
Smaller ε = more noise = stronger privacy. The app's default budget is ε = 1.0
(the recommended ceiling in INDUSTRY_MIN; clamped to 0.05–10). Negative draws are
clamped to 0 and the result is rounded to an integer (both are standard
post-processing).
Primary source: Dwork, McSherry, Nissim & Smith, Calibrating Noise to Sensitivity (2006); Dwork & Roth (2014); US Census Bureau 2020 Disclosure Avoidance System.
Simplified — read this before relying on DP. UnitTrack's DP is illustrative, not a production privacy guarantee, and suppression (§2.1–2.3) is the default governance path. Specifically:
- The RNG is deterministically seeded (
np.random.default_rng(seed=42), PCG64 — not cryptographic), so a given view's noise is reproducible. The module's own docstring notes "production DP would draw fresh randomness each release." A fixed, known seed weakens the formal guarantee as deployed. - Counts only. Only
dp_countsexists (Δ = 1); there is no mechanism for sums or means, so DP does not cover value/consumption magnitudes. - Pure (ε, 0)-DP (Laplace, δ = 0). No (ε, δ) relaxation.
- No privacy-budget accounting. Each query uses the ε passed in; there is no composition/accountant tracking cumulative spend across queries.
- Disjoint groupings only. Per-cell Laplace on a histogram is ε-DP under
parallel composition — each customer must land in exactly one cell. The DP tab
therefore offers only disjoint groupings (customer class, geography, service type)
and not time grains (month/quarter/year), where one customer spans many cells
so the true guarantee would be (T·ε)-DP, not ε. Enforced server-side (
DP_GROUPINGS,dp_summary), not just in the dropdown. - True counts are computed alongside the noised ones in the engine result. The web view withholds the true counts for real (non-sample) datasets — it publishes only the noised value (regression-tested); the true value is shown only for the public demo data, as a teaching aid.
2.7 Safe-by-default, warn-on-override
Disclosure control is global governance, not a per-analysis afterthought. The
default policy is the industry-accepted minimum (INDUSTRY_MIN, anchored to
CPUC 15/15): k = 15, single-customer dominance cap p = 0.15, complementary
suppression on, tail-coding on, DP budget ceiling ε = 1.0, and a 15-account floor
to publish a whole-population model.
Loosening any control toward higher risk raises a warning; tightening is
silent (policy_warnings, direction-aware). Warnings are advisory (yellow); an
actual active disclosure is decided red from the analysis result itself. A
whole-population model can never be published below a 5-account hard floor, even
with acknowledgment.
Primary source: CA CPUC D.14-05-016 ("15/15") as the sector's minimum; ESSnet SDC Handbook for the control definitions.
3. Analyses
Every analysis below runs on the customer's own ingested reads. In the standalone
analyses (§3.2–§3.7) consumption is taken as a magnitude (abs()), so a
reversed/negative read contributes size, not sign (the data-quality view, §3.7,
separately counts the negatives); the base tabulation (§3.1) sums the signed
value as stored, so a negative read there lowers the total. Dates parse
YYYY-MM-DD, MM/DD/YYYY, or YYYY/MM/DD. Percentiles use NumPy's default linear
interpolation.
3.1 Consumption tabulation
The base analysis: group reads by customer class, geography, service type, or a
time grain, and publish count, mean, or sum per group (§1), through the full
SDC pipeline (§2). A crosstab is the two-dimension form of the same
tabulation, run through the same pipeline — including complementary suppression
across the table's margins.
3.2 Efficiency benchmarking — EUI
Answers: how efficient is each building versus its same-class peers?
(udap_engine/benchmark/__init__.py.)
Per building, energy is converted to a common unit (1 kWh = 3.412 kBtu,
1 therm = 100 kBtu), summed, annualized, and divided by floor area:
annual_kbtu = Σ|kbtu| · 365 / span_days (span_days ≥ 60 required)
EUI = annual_kbtu / floor_area (kBtu/ft²/yr)
Per class (default k = 15), it reports the median, 25th, 75th percentile, and
mean EUI; classes with fewer than k buildings are suppressed (but still count
toward the overall median and building total).
Source energy is reported alongside site EUI: source_EUI = site_EUI ×
source-site ratio (grid electricity 2.80, natural gas 1.05; EPA Source
Energy Technical Reference). A coarse national context ratio is also shown —
the class median site EUI as a percent of the sector's all-fuel national-average
site EUI (RECS 2020 residential / CBECS 2018 commercial). It carries no
efficiency verdict: the reference is all-fuel while a dataset is usually one
commodity, so it's context, not judgment.
Version-pinned constants. All reference numbers (source-site ratios, national
EUIs, survey vintages) live in udap_engine/benchmark/constants.py under a single
BENCHMARK_CONSTANTS_VERSION. Maintenance: when EIA publishes a new
CBECS/RECS or EPA revises the ratios, update the value + its cited source there,
bump the version (YYYY.N), and note it in the module changelog — nothing else
hard-codes these numbers.
Primary source: EPA ENERGY STAR (the EUI / peer-percentile concept) and EPA Source Energy Technical Reference (site→source ratios); site energy conversions (1 kWh = 3.412 kBtu, 1 therm = 100 kBtu) per EPA Thermal Energy Conversions / EIA Monthly Energy Review Appendix A; and the national-context sector figures from EIA CBECS 2018 / RECS 2020 — which are means (distinct from ENERGY STAR's property-type medians), so the coarse "vs national avg" ratio is anchored to CBECS/RECS, not to the official ENERGY STAR benchmark.
Simplified. This is within-dataset EUI + peer percentiles, source EUI, and
a coarse national anchor — not the official ENERGY STAR 1–100 score, which is
a property-type, operationally-adjusted CBECS regression (needs worker/hours/etc.
inputs and EPA's licensed model) we do not replicate. Energy content applies to
electric and gas only; water volume is excluded (applicable = False).
3.3 Peak demand & load factor
Answers: how peaky is demand, and how much do customer peaks coincide?
(udap_engine/demand/__init__.py; electric only — needs a peak_demand_kw
column.)
load_factor = average_load_kw / peak_load_kw (0–1)
coincidence = coincident_peak / Σ individual peaks (0–1)
diversity = 1 / coincidence (≥ 1)
Per-class load-factor median/quartiles use the same k = 15 suppression. Load
factors outside (0, 1] are dropped.
Primary source: NREL Uniform Methods Project Ch. 10 (Peak Demand) for load factor and peak demand. The coincidence and diversity factors here use the classic power-systems definitions (diversity = 1/coincidence); note different sources (e.g. BPA) define "coincidence factor" slightly differently, so treat the exact figure as convention — and see the directional-bias caveat below.
Simplified — and biased in a known direction. The coincident peak is an
approximation: each account's single maximum peak is placed entirely in its
peak_hour bucket and zero in every other hour, rather than counting each
account's actual (nonzero) load at the system-peak hour — which requires interval
data we don't have. Because non-peaking customers' real load at that hour is treated
as zero, the coincident peak is systematically understated, so the reported
coincidence factor is biased low and diversity factor biased high. Treat these
two as directional/indicative only; load factor and per-account peaks are unaffected.
A true coincidence factor needs time-aligned interval (AMI) demand.
3.4 Seasonal / outdoor use — winter baseline
Answers: how much use is seasonal (e.g. outdoor irrigation for water)?
(udap_engine/seasonal/__init__.py.)
Reads are pooled by calendar month (across accounts and years). The baseline is the lowest monthly average; consumption above baseline is the seasonal component:
baseline = min over months of monthly_avg
outdoor = Σ_months max(0, monthly_avg − baseline)
outdoor_pct = 100 · outdoor / Σ monthly_avg
Requires ≥ 4 distinct months. Water (CCF) is converted to gallons for the daily
figure (1 CCF = 748 gal).
Primary source: the minimum-month / winter-averaging logic follows AWWA conservation practice (winter-quarter/base-month averaging). EPA WaterSense and the Water Research Foundation's Residential End Uses of Water support the premise that outdoor use is a large, separable share — they do not define this specific subtract-the-lowest-month formula (WaterSense uses a landscape water budget; WRF used flow-trace disaggregation). 1 CCF = 748 gal (standard water-utility rounding, 748.05).
Simplified. This is the minimum-month method, not a degree-day (HDD/CDD) weather-normalized regression: "winter" is nominal (whatever month is lowest), the baseline is a single month, and months are pooled across years without per-account normalization. Sensitive to one anomalous low month.
3.5 Consumption distribution & inequality
Answers: how unequal is consumption across customers?
(udap_engine/distribution/__init__.py; withheld entirely below 5 accounts.)
On annualized per-account consumption:
Gini = (2·Σ i·xᵢ)/(n·Σx) − (n+1)/n (xᵢ ascending, 1-based i; 0 = equal)
CV = std(x) / mean(x) (population std, ddof = 0)
high outliers = #{ xᵢ > Q3 + 1.5·IQR } (Tukey)
plus p10/p25/median/p75/p90 and a downsampled Lorenz curve.
Primary source: Gini (1912); Lorenz (1905); Tukey (1977) for the IQR rule.
Simplified. CV uses the population standard deviation; the outlier count is
the upper fence only (low outliers not counted). Optional tail-coding (off by
default) withholds p10/p90 when a decile holds fewer than k = 15 accounts
(ESSnet extreme-value protection; NIST SP 800-188 top/bottom-coding).
3.6 Spatial autocorrelation — Moran's I
Answers: is consumption geographically clustered, dispersed, or random?
(udap_engine/spatial/__init__.py; needs ≥ 4 valued areas.)
Global Moran's I with binary rook-contiguity weights w over the published areas:
I = (n / S0) · ( Σᵢ Σⱼ wᵢⱼ (xᵢ−x̄)(xⱼ−x̄) ) / ( Σᵢ (xᵢ−x̄)² )
E[I] = −1 / (n−1)
z = (I − E[I]) / √Var(I) Var per Cliff & Ord normality assumption
p = erfc(|z| / √2) two-tailed normal
p < 0.05 → Clustered (if I > E[I]) or Dispersed; else Random.
Primary source: Moran (1950) for the global statistic; Cliff & Ord, Spatial Autocorrelation (1973) for the normal-theory inference.
Local statistics (WHERE the clusters are). Global Moran's I says whether the map clusters; the local statistics say where. For each area we compute:
- Getis-Ord Gi* (Getis & Ord 1992; Ord & Getis 1995) — a closed-form z-score of
the neighborhood-plus-self sum vs. the global mean:
Gi* = (Σ_{j∈N_i∪{i}} x_j − X̄·w_i) / (S·√((n·w_i − w_i²)/(n−1))), wherew_iis the star size andSthe population SD.Gi* > +1.96marks a significant hot spot (high-value cluster),< −1.96a cold spot. Deterministic — no permutation. - Local Moran's I quadrant (Anselin 1995) —
I_i = (x_i−X̄)/m₂ · Σ_j w_ij(x_j−X̄)with the HH / LL / HL / LH label (HH/LL are clusters, HL/LH are spatial outliers).
The Analysis › Geography tab reports the count of significant hot/cold spots; the
per-area detail is available in the engine (local_spatial_grid / _graph).
Simplified. The global p-value is the analytic normal-theory approximation,
not a Monte-Carlo permutation (pseudo) p-value; likewise Gi uses its analytic normal
z-score. The local Moran quadrant labels cluster type descriptively (its own
permutation-based significance is not yet computed — Gi provides the tested hot/cold
inference). Weights are binary rook contiguity (edge-sharing, not queen/diagonal)
and not row-standardized, which differs from the row-standardized convention in
many GIS packages. Islands (no neighbors) still count toward n.
3.7 Data quality
Answers: what's wrong with the data, surfaced not hidden?
(udap_engine/quality/__init__.py.) Counts and percentages of: estimated reads
(flag in a fixed truthy set), negative usage (consumption < 0), missing
coordinates (lat or lon null/blank), and high statistical outliers
(> Q3 + 1.5·IQR, Tukey, computed on ≥ 4 values). Bad rows are flagged, not
dropped.
Primary source: Tukey (1977) for the IQR rule.
Simplified. Outliers are upper-fence only; missing-geo flags only null/empty (not a malformed non-empty coordinate); the estimated-flag set is a fixed literal list.
3.8 Weather normalization — degree-day regression
Answers: how much of consumption is weather-driven, and what would it be in a
normal-weather year? (udap_engine/weather/__init__.py.) Monthly consumption is fit
to heating and cooling degree days by least squares:
E = β₀ + β_h·HDD + β_c·CDD (base 65°F; ≥ 3 months required)
HDD = max(0, 65 − T̄)·days, CDD = max(0, T̄ − 65)·days
β₀ is the weather-independent base load; β_h / β_c are the heating / cooling slopes. The model restates annual use at normal-year degree days and reports the weather-sensitive share of consumption. Temperatures come from the measured NOAA GHCN-Daily nearest station when a token is configured, otherwise a modeled climatology — which one was used is labeled in-app.
Primary sources: ASHRAE Guideline 14-2014; PRISM (Princeton Scorekeeping Method) (Fels 1986); base-65°F degree days per US EIA / NOAA; NOAA GHCN-Daily (Menne et al. 2012).
Gas (heating-only model). For gas datasets (therms / CCF) — which have no
cooling load — the model drops the cooling term and fits the heating-only form
E = β₀ + β_h·HDD (heating_only=True, auto-selected by fuel unit). β₀ is then
non-weather gas use (water heating, cooking) and the HDD-sensitive part is space
heating. This is more robust than forcing a spurious cooling coefficient onto a
heating-only fuel, and is the original PRISM formulation (Fels 1986 developed the
Princeton Scorekeeping Method for gas heating). The workbench labels the gas model
as heating-only.
Simplified. Mean-temperature degree days (not hourly integration); a single fixed base temperature (65°F), not a fitted balance point; requires ≥ 3 months to fit.
3.9 Measurement & verification — IPMVP Option C
Answers: how much energy did an efficiency measure actually avoid, adjusted
for weather? (udap_engine/mv/__init__.py.) The history is split at an
intervention date into a baseline and a reporting period. A weather
model E = β₀ + β_h·HDD + β_c·CDD is fit on the baseline months (least squares,
the same degree-day regression as weather normalization), then projected onto
the reporting period's actual degree days to give the adjusted baseline —
the Customer Baseline Load (CBL), i.e. what the site would have used had nothing
changed. Avoided energy is the difference from metered use:
Avoided energy = Σ (adjusted baseline) − Σ (reporting actual)
reported in absolute units and as a percent of the adjusted baseline. Needs ≥ 3 baseline months (to fit) and ≥ 1 reporting month.
Primary sources: IPMVP Core Concepts (EVO 10000-1:2016), Option C (whole-facility); ASHRAE Guideline 14-2014 (whole-building regression baseline); base-65°F degree days per US EIA / NOAA.
Simplified / honest scope. The routine adjustment is degree-days only (temperature), so it applies to weather-sensitive energy (electricity, gas), not water; non-routine adjustments (occupancy, schedule changes) are not modeled; and on the synthetic demo data — which has no real retrofit — avoided energy reads near 0% by construction, which is the method behaving correctly.
3.10 Energy burden & equity
Answers: what share of household income goes to energy, and where is it
unaffordable? (udap_engine/burden/__init__.py.) Per census tract, each
household's annualized consumption is priced at an average retail rate and divided
by the tract's median household income:
burden%ₕₒᵤₛₑₕₒₗd = 100 · (annual kWh · $/kWh) ÷ tract median income tract burden = median household burden in the tract
Tracts are classified ≤ 6% affordable · 6–10% moderate · > 10% high. Tracts
with fewer than k accounts, or no income on record, are suppressed (the same
minimum-count rule as the maps).
Primary sources: DOE/NREL LEAD tool (tract-level burden); the 6% affordability cut point from the Home Energy Affordability Gap (Fisher, Sheehan & Colton), used by DOE/NREL LEAD and ACEEE (>10% = "severe"); US Census ACS 5-year B19013 (median household income); EIA average retail price (≈16.0¢/kWh US residential 2023). The affordable/moderate/high labels are ours; the cut points match ACEEE.
Simplified / honest scope. This is the burden from the metered commodity (this dataset's electricity or gas) — a lower bound on total home-energy burden (which sums all fuels). Income is tract-level (ACS median), so household burden varies only by consumption within a tract (the standard LEAD-style approximation, not household income). The default rate is an EIA state/US average — real deployments pass the utility's own rate. The federal Justice40 / CEJST screen was rescinded in 2025, so high-burden tracts are flagged directly from the data rather than via that (now-defunct) overlay.
3.11 Standard report & interchange formats
Answers: can the outputs drop into the formats utilities already file and
exchange? (udap_engine/reports/__init__.py.) All three are emitted from the same
disclosure-controlled gold cells and apply the minimum-count rule:
- EIA-861 — Sales to Ultimate Customers schedule: customers, annualized sales
(MWh), revenue ($1,000), and average price (cents/kWh) by end-use sector
(residential / commercial / industrial). Sectors below
kcustomers are suppressed. Electric only. - EIA-176 — the natural-gas analog: annualized deliveries to consumers by sector
in Mcf (10 CCF = 1 Mcf exact; therms via EIA heat content, 1 Mcf ≈ 10.37 therms),
with customers, revenue ($1,000), and average price ($/Mcf). Sectors below
kcustomers are suppressed. Gas only. - Green Button — Aggregated Data (ESPI) — a well-formed ESPI Atom feed
(
UsagePoint·MeterReading·ReadingType·IntervalBlock) of aggregated monthly usage in watt-hours (ESPI unit 72); months belowkcontributing accounts are omitted. - ENERGY STAR SEP data — per-building site EUI vs. same-class median (the data
behind a Statement of Energy Performance); classes below
kbuildings are suppressed. - Utility load-research report — the load & demand analysis packaged as a report:
system load factor, coincident peak, coincidence & diversity factors, and the
per-rate-class load-factor distribution (median / P25 / P75). Classes below
kbuildings are suppressed. Electric with interval peak-demand data only. - Energy performance scorecard — per-class median site and source EUI vs.
the sector national reference (% of national), plus the portfolio median. Classes
below
kbuildings are suppressed. Honest scope: an EUI-vs-reference scorecard (CBECS / RECS references; EPA source-site ratios), not the proprietary EPA ENERGY STAR 1–100 score.
Primary sources: US EIA Form EIA-861; NAESB REQ.21 ESPI / Green Button; EPA ENERGY STAR SEP.
Simplified / honest scope. EIA-861 annualizes a partial year by read span; the Green Button feed carries the aggregate group total (an ADS profile), not per-customer intervals; the SEP export is the performance data — the official signable 1–100 scored SEP awaits the ENERGY STAR score.
4. Geography
Groupings and Moran's I operate on real Census tracts where a boundary layer
covers the data: accounts are assigned to the tract their point falls in
(point-in-polygon), using a bundled Travis County (Austin) layer for the demo and
Census TIGERweb layers fetched at ingest for other regions (all public domain).
Where no layer covers the data, UnitTrack falls back to a 6 × 6 square grid
(GEO_GRID). Moran's rook-contiguity graph is built over whichever areas result
(udap_app/core/analysis.py).
5. What is simplified or not yet done
Collected honestly in one place:
- Complementary suppression is a smallest-value heuristic, not the ESSnet Handbook's information-loss-minimizing linear program (§2.3).
- Differential privacy is illustrative — deterministically seeded RNG, counts only, (ε,0), no budget accounting (§2.6). Suppression is the production default.
- Coincident peak is a bucketed approximation, not a true time-aligned interval coincidence (§3.3).
- Seasonal split is the minimum-month method, not a degree-day weather regression (§3.4).
- EUI is within-dataset, not the CBECS-calibrated ENERGY STAR 1–100 score (§3.2).
- Moran's p-value is normal-theory, not permutation-based; weights are binary rook, not row-standardized (§3.6).
- Distribution/quality outliers are upper-fence only (§3.5, §3.7).
6. Citations at a glance
| Method | Primary source(s) |
|---|---|
| Minimum-count (k-anonymity) | Sweeney (2002); ESSnet/Eurostat SDC Handbook §3 |
| (n,k)-dominance | ESSnet SDC Handbook §5.2 (Hundepool et al.) |
| Complementary suppression | ESSnet SDC Handbook §5.4; ASA/SRMS cell-suppression overview |
| Re-identification risk score | UnitTrack construct on Sweeney (2002) worst-case; threat basis: AMI/NILM re-id literature |
| Regulatory thresholds | CA CPUC D.14-05-016 ("15/15"); IL Utility Data Access Act, 220 ILCS 33 |
| Differential privacy | Dwork et al. (2006); Dwork & Roth (2014); US Census 2020 DAS |
| Efficiency benchmarking (EUI) | EPA ENERGY STAR Score Technical Reference; DOE/EIA CBECS |
| Peak demand / load factor | NREL UMP Ch. 10; BPA M&V Peak Demand guide |
| Seasonal / outdoor use | EPA WaterSense; AWWA; Water Research Foundation |
| Distribution & inequality | Gini (1912); Lorenz (1905); Tukey (1977) |
| Spatial autocorrelation (global Moran's I) | Moran (1950); Cliff & Ord (1973) |
| Data quality | Tukey (1977) |
Fuller provenance, including confidence tags (verified / sourced / gap), is in
docs/RESEARCH_ANALYSIS_METRICS.md and docs/DISCLOSURE_CONTROL_DESIGN.md.