All writing

SPSS Rounds Your Weighted Counts. Here's How Much That Actually Matters

Less than you have been told — about 0.09pp on a base of 400. But the rounding is a symptom of a much larger error sitting directly underneath it, and that one is worth 26%.

Run a weighted crosstab in SPSS and the cell counts come back as integers. The underlying weighted count of 47.3 is displayed, and used, as 47.

This gets raised periodically as a serious data quality problem. I wanted to know how serious, so I measured it — and the honest answer is: real, small, and much less important than the thing sitting right next to it.

That combination makes it worth writing up, because "this is a smaller problem than you think, and here is the bigger one" is more useful than another warning.


Part 1 — How much does rounding move a percentage?

A weighted cell count is a real number. Rounding it to an integer perturbs it by at most 0.5. That error propagates into the percentage:

with both numerator and denominator perturbed, so the effect is roughly per cell in the worst case.

Simulated: multinomial cell allocations with lognormal weights of mean 1, comparing percentages from exact weighted counts against percentages from rounded ones. 20,000 replications per row. The figure reported is the largest shift across the cells of the table.

BaseCellsMean max shift95th percentileWorst observedSignificance verdict flips
1,00060.042pp0.059pp0.096pp0.00%
40040.090pp0.140pp0.217pp0.10%
20040.181pp0.282pp0.496pp0.37%
10030.306pp0.526pp0.854pp0.69%

On a base of 1,000 the largest distortion anywhere in the table averages 0.042 percentage points. On a base of 400, 0.09pp. Even at a base of 100 the worst case observed across 20,000 simulated tables was 0.85pp.

The last column is the practically relevant one: how often does rounding flip a chi-square test across the 5% threshold? At a base of 400, once in a thousand tables. At a base of 100, seven times in a thousand.

This is a real effect and a small one. If someone tells you their published percentages are wrong because SPSS rounded the weighted counts, they are right, and the error is in the second decimal place of a number that is usually reported to zero decimals.


Part 2 — The thing underneath it

So why does the rounding happen at all? Because WEIGHT BY in SPSS's base module implements frequency weights, not sampling weights. A frequency weight of 3 means "this row occurred 3 times", and a count of occurrences is necessarily an integer. Rounding is not a display choice; it is what the frequency-weight abstraction requires.

That same abstraction is what produces the much larger error documented in the sampling weights article: if a weight of 3 means three observations, then the weighted sample size is the number of independent observations, and every standard error is computed from it. That is the error worth caring about, and its magnitude in the worked example there is 26% — three orders of magnitude larger than the rounding.

Put the two side by side on a base of 400:

Consequence of treating sampling weights as frequency weightsMagnitude
Cell counts rounded, percentages shift~0.09pp
Standard errors computed from the weighted ~26% too small

The rounding is the visible symptom. The standard errors are the disease. And the rounding is useful precisely because it is visible: if your software is rounding weighted counts, that is a reliable signal that it is also treating your weights as frequencies for inference. Treat rounded counts as a smoke alarm rather than a fire.


Part 3 — When rounding does matter

Three cases where the small effect stops being negligible:

Very small cells. The perturbation is bounded by 0.5 in absolute terms, so its relative size grows as the cell shrinks. A weighted count of 3.4 rounded to 3 is a 12% relative error on that cell. This is a further reason not to report percentages on tiny bases, on top of the interval problems.

Grossed-up weights. If weights are scaled to population totals rather than to the sample size, the counts are large and rounding is irrelevant. If they are scaled to sum to 1, rounding destroys the table entirely — every cell becomes 0. This is one more reason the scale of your weights should never affect your results, and a good smoke test: multiply all weights by 1,000 and check nothing changes.

Chains of derived figures. Nets, indices and derived measures computed from already-rounded counts compound the error. Compute derived figures from unrounded values and round once, at display time.


Part 4 — Implementation

The fix is the same fix as for the standard errors: stop using frequency-weight machinery for sampling weights.

SPSS — use Complex Samples, which handles sampling weights natively and does not round:

* Wrong for sampling weights: rounds counts, and computes SEs from the
  weighted n.
WEIGHT BY w.
CROSSTABS TABLES = outcome BY subgroup /STATISTICS = CHISQ.

* Right: declare the design once, then use the CS procedures.
CSPLAN ANALYSIS
  /PLAN FILE = 'design.csaplan'
  /PLANVARS ANALYSISWEIGHT = w
  /DESIGN STRATA = stratum CLUSTER = psu
  /ESTIMATOR TYPE = WR.

CSTABULATE
  /PLAN FILE = 'design.csaplan'
  /TABLES VARIABLES = outcome BY subgroup
  /CELLS ROWPCT
  /STATISTICS SE CIN(95)
  /TEST INDEPENDENCE.

CTABLES also avoids the rounding for display purposes, but it does not fix the inference — for that you need Complex Samples.

R — the question does not arise, because survey never converts weighted totals to counts:

library(survey)
des <- svydesign(ids = ~psu, strata = ~stratum, weights = ~w, data = dat)

svytable(~outcome + subgroup, des)              # unrounded weighted totals
prop.table(svytable(~outcome + subgroup, des), 2)
svychisq(~outcome + subgroup, des, statistic = "F")   # Rao-Scott

Watch for round = TRUE in svytable(), which exists for compatibility with functions expecting integer tables. Leave it off.

A smoke test that catches both problems at once:

def scale_invariance_check(analysis_fn, data, weights, factor=1000):
    """Multiply every weight by a constant and re-run.

    Correct methods are scale-invariant. If percentages move, counts are
    being rounded. If p-values move, the weights are being treated as
    frequencies - which is the far bigger problem.
    """
    base = analysis_fn(data, weights)
    scaled = analysis_fn(data, weights * factor)
    return {
        "pct_moved": abs(base["pct"] - scaled["pct"]) > 1e-6,
        "pvalue_moved": abs(base["p"] - scaled["p"]) > 1e-9,
    }

Two minutes, and it detects the entire class of errors.


Part 5 — Checklist

  1. Do not panic about rounded counts. On any base you should be reporting on, the distortion is under a tenth of a percentage point.
  2. Do treat them as a signal. Rounding means frequency-weight machinery, which means the standard errors are wrong by far more.
  3. Run the scale-invariance check. Multiply every weight by 1,000. Percentages moving means rounding; p-values moving means something much worse.
  4. Never scale weights to sum to 1 if anything downstream rounds counts.
  5. Compute derived figures from unrounded values and round once at display.
  6. Use Complex Samples in SPSS, or survey in R, for anything with a p-value or an interval attached.
  7. Keep the priorities straight. If you have limited time to fix a survey pipeline, the standard errors are worth roughly 300 times what the rounding is.

Reproducing the simulation

import numpy as np
from scipy import stats
rng = np.random.default_rng(160235)

def rounding_effect(n, n_cells, reps=20000):
    shifts, flips = [], 0
    for _ in range(reps):
        probs = rng.dirichlet(np.ones(n_cells) * 4)
        counts = rng.multinomial(n, probs)
        w = np.exp(rng.normal(0, 0.5, n) - 0.5**2 / 2)   # lognormal, mean 1
        labels = np.repeat(np.arange(n_cells), counts)
        rng.shuffle(w)

        wc = np.array([w[labels == k].sum() for k in range(n_cells)])
        wc_r = np.round(wc)
        shifts.append(np.abs(wc/wc.sum()*100 - wc_r/wc_r.sum()*100).max())

        exp, exp_r = wc.sum()/n_cells, wc_r.sum()/n_cells
        chi_u = ((wc - exp)**2 / exp).sum()
        chi_r = ((wc_r - exp_r)**2 / exp_r).sum()
        crit = stats.chi2.ppf(0.95, n_cells - 1)
        flips += (chi_u > crit) != (chi_r > crit)
    return np.mean(shifts), np.max(shifts), flips / reps

print(rounding_effect(400, 4))    # (0.090, 0.217, 0.0010)
print(rounding_effect(100, 3))    # (0.306, 0.854, 0.0069)

Sources and further reading

  • IBM SPSS Statistics documentation, Weight Cases — states that the base module's weighting is frequency-based and directs users to Complex Samples for sampling weights.
  • Lumley, T. (2010) Complex Surveys: A Guide to Analysis Using R, Chapter 2 — svytable and why weighted totals are not counts.
  • Heeringa, S., West, B. & Berglund, P. (2017) Applied Survey Data Analysis, 2nd edn, Chapter 4 — the distinction between frequency, analytic and sampling weights across the major packages.