Choosing a Rounding Strategy for Published Rates
This guide, part of Precision Standards in Epi-Mapping, treats the number of digits in a published rate as a decision with two consequences: what it claims about precision, and what it lets a reader recover.
Problem Context & Constraints
A tract rate of 47.3829 per 100,000 is a statement that the rate is known to four decimal places. It is not; with a numerator of eleven cases its 95% interval spans roughly 24 to 85. Every digit beyond the first or second is noise presented as information, and it invites readers to rank tracts by differences that the data cannot support.
Excess precision has a second consequence that matters more. A rate is a count divided by a population, and if the population is published — or can be looked up in a public census table — a rate with enough decimal places can be multiplied back to recover the count exactly. A table that suppresses a count of three and publishes the rate to four decimals has published the three.
Three constraints shape a rounding strategy. Digits should reflect uncertainty, so a rate is rounded to a precision its interval can support. Rounding must not re-enable back-calculation of suppressed or small counts, which means the rounding granularity must exceed the change in rate that one case produces. And tables that must add up — counts by area that sum to a published total — need controlled rounding, because rounding each cell independently makes the published cells disagree with the published total.
Prerequisites
python3.11pandas2.2.2,numpy1.26.4- Rates with their counts, denominators and confidence intervals, before any rounding
- The disclosure policy’s small-cell threshold
- Knowledge of which denominators are publicly available, since those are the ones that enable back-calculation
Step-by-Step Solution
Compute, per area, the change in rate that one case produces; set the rounding granularity above it; check that rounding also matches the rate’s own uncertainty; and use controlled rounding where published cells must sum to a total.
# Rounding published rates to their precision without enabling back-calculation.
# Pinned: pandas==2.2.2, numpy==1.26.4
import logging
import math
import numpy as np
import pandas as pd
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("rounding")
PER = 100_000
NICE_STEPS = [0.1, 0.5, 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500]
def one_case_step(pop: pd.Series) -> pd.Series:
"""Change in the published rate caused by a single case."""
return PER / pop.replace(0, np.nan)
def granularity(rates: pd.DataFrame, pop_col: str = "population",
ci_lo: str = "lo", ci_hi: str = "hi") -> pd.DataFrame:
"""Rounding step per area: coarser than one case, and no finer than the interval."""
out = rates.copy()
step_case = one_case_step(out[pop_col])
# Uncertainty-based precision: a tenth of the interval width is a generous ceiling.
step_ci = (out[ci_hi] - out[ci_lo]) / 10.0
need = np.maximum(step_case * 1.01, step_ci)
out["round_to"] = [next((s for s in NICE_STEPS if s >= n), NICE_STEPS[-1])
if np.isfinite(n) else np.nan for n in need]
out["rate_published"] = (out["rate"] / out["round_to"]).round() * out["round_to"]
log.info("rounding steps assigned: %s", out["round_to"].value_counts().sort_index().to_dict())
return out
def back_calculation_check(pub: pd.DataFrame, pop_col: str = "population",
count_col: str = "cases") -> pd.DataFrame:
"""Can the published rate and public population pin the count to one integer?"""
lo = (pub["rate_published"] - pub["round_to"] / 2) * pub[pop_col] / PER
hi = (pub["rate_published"] + pub["round_to"] / 2) * pub[pop_col] / PER
candidates = np.floor(hi) - np.ceil(lo) + 1
pub = pub.assign(count_candidates=candidates)
pinned = pub["count_candidates"] <= 1
if pinned.any():
log.error("%d published rates pin their count to a single value", int(pinned.sum()))
else:
log.info("no published rate identifies its count uniquely")
return pub
def controlled_round(cells: pd.Series, total: int, base: int = 5) -> pd.Series:
"""Round counts to a base so the rounded cells sum to the rounded total.
Cells are rounded down, then the remainder is distributed to the cells with
the largest fractional parts — a deterministic largest-remainder scheme.
"""
q = cells / base
down = np.floor(q)
target = int(round(total / base))
short = target - int(down.sum())
order = (q - down).sort_values(ascending=False, kind="mergesort").index
add = pd.Series(0, index=cells.index)
add.loc[order[:max(short, 0)]] = 1
out = ((down + add) * base).astype(int)
assert out.sum() == target * base
return out
Setting the step from the one-case effect, not from a fixed number of decimals, is the design point. A fixed rule — one decimal place everywhere — is too coarse for a large county, where one case moves the rate by a fraction of a unit, and dangerously fine for a small tract, where one case moves it by dozens. Deriving the step per area gives each rate the precision it can safely carry.
The back-calculation check verifies the rule rather than trusting it. For each published rate it computes how many integer counts are consistent with the rounded value and the public population; any rate that admits only one count has disclosed that count, whatever the rounding rule intended.
Validation & Edge Cases
Rounding a suppressed count’s neighbours. Rounding does not replace suppression. A cell below the small-cell threshold is suppressed first; rounding applies to what remains, and the back-calculation check confirms the combination is safe.
Public denominators you did not publish. A rate paired with a population you withheld is still back-calculable if the population is in a public census table. Assume every census and survey population is available to the reader.
Rounding ties. Round-half-to-even and round-half-up differ at exact halves, which occur more often than expected with small integers. Choose one, record it, and apply it everywhere; the controlled rounding above uses a stable sort so ties resolve deterministically.
Rates near zero. Rounding a small positive rate to zero states that the rate is zero, which is false and may imply suppression where none occurred. Publish “less than” the smallest step instead of zero.
Percentages and proportions. The same logic applies: a proportion of 33.33% from a denominator of three is a disclosure. The one-case step for a proportion is one over the denominator.
INFO rounding rounding steps assigned: {0.5: 58, 1: 214, 5: 611, 10: 402, 50: 188}
ERROR rounding 4 published rates pin their count to a single value
Those four rates are exactly the cases the check exists for: tracts with very large public populations where even a coarse step is finer than one case’s effect on the rate would suggest, because the interval-based step dominated. Raising their step to the next value in the list, or rounding the population used for the rate as well, resolves them; the choice should be recorded against each area.
Compliance Notes
Record the rounding rule — the one-case criterion, the interval ceiling, the list of permitted steps, the tie-breaking rule and the controlled-rounding base — as configuration, and record per area the step applied and the result of the back-calculation check. Any published table with rounded rates should state the rounding rule in a footnote, because a reader comparing your rates with an unrounded source needs to know the difference is deliberate.
Rounding is a disclosure control as well as a presentation choice, and it belongs in the disclosure review alongside suppression. The two interact — a suppressed cell with rounded neighbours can still be recovered from a row total — so they should be evaluated together over the whole release, following Detecting Differencing Attacks Across Published Tables.
Related Topics
- Precision Standards in Epi-Mapping — the parent guide on precision governance for coordinates and values.
- Setting Decimal Precision for Disease Coordinate Mapping — the same principle applied to coordinates.
- Small-Count Cell Suppression in Rate Maps — the suppression rounding must work alongside.