cassionData Analysis

Lesson 7 of 8

Unit · What you may publish

The interval, and the sentence around it

Why the textbook interval gives 0.88% for a proportion that cannot go below zero, the logit interval that fixes it, degrees of freedom you have 72 of, and why two overlapping intervals do not mean no difference.

PythonR120 minDemographic and Health Survey (DHS)Multiple Indicator Cluster Survey (MICS)SMART surveySustainable Development Goals (SDG)

What the interval is for

A survey estimate is a guess about a population, made from a sample. The confidence interval is the honest width of that guess, and publishing the point estimate without it is the single most common misuse of survey data in this sector.

The formal reading — “95% of intervals constructed this way would contain the true value” — is correct and unhelpful in a meeting. The operational reading is what you need: the interval is the set of population values that would not be surprising given this sample. If a threshold sits inside your interval, your survey cannot say which side of it the population is on.

The textbook interval, and where it fails

p +/- z * se

Fine in the middle of the range and wrong at the ends. Take severe acute malnutrition among the measured children:

p, se = 0.0217, 0.0066
print(f"Wald: {p - 1.96 * se:.2%} to {p + 1.96 * se:.2%}")
p <- 0.0217; se <- 0.0066
c(p - 1.96 * se, p + 1.96 * se)

2.17%, interval 0.88% to 3.46%. Symmetric, and the symmetry is the problem: a proportion cannot be negative, and with a slightly smaller estimate this interval would extend below zero and be published anyway.

The logit interval

Transform to the log-odds scale, build the interval there, transform back.

import math

logit = math.log(p / (1 - p))
se_logit = se / (p * (1 - p))
low, high = (logit - 1.96 * se_logit, logit + 1.96 * se_logit)

print(f"logit: {1 / (1 + math.exp(-low)):.2%} to {1 / (1 + math.exp(-high)):.2%}")
library(survey)
svyciprop(~I(child_muac_mm < 115), child_design, method = "logit")

1.19% to 3.92%. Asymmetric — further above the estimate than below — which is the correct shape for a small proportion, and it cannot escape the 0–1 range whatever the estimate.

Use the logit interval for any proportion below about 10% or above 90%. In the middle the two agree to a decimal place and it does not matter. svyciprop(..., method = "logit") in R does it directly; the Python examples in this course compute it as above.

Degrees of freedom you actually have

The 1.96 assumes a normal distribution, which assumes plenty of independent observations. In a cluster survey the independent units are the clusters, not the households.

df = number of PSUs - number of strata
clusters = survey["ea_id"].nunique()
strata = survey["stratum"].nunique()
print(f"{clusters} clusters, {strata} strata, df = {clusters - strata}")
c(clusters = n_distinct(survey$ea_id), strata = n_distinct(survey$stratum))
degf(design)

Seventy-five clusters, three strata, 72 degrees of freedom, and t(0.975, 72) is 1.993 rather than 1.96. A difference of 2% in the interval width, which here is irrelevant and is exactly the kind of thing that stops being irrelevant fast: a survey with twelve clusters has nine degrees of freedom and a multiplier of 2.26, which is 15% wider than the normal approximation.

Use the t multiplier, and report the degrees of freedom. It costs nothing and it tells a reader how many clusters you had without them having to ask.

Overlapping intervals are not a test

The most consequential misreading in this lesson.

for stratum in ["urban", "rural-accessible", "rural-remote"]:
    print(stratum)   # displaced households
Stratum Displaced 95% interval
Urban 15.6% 12.0 – 19.2%
Rural accessible 8.0% 4.7 – 11.3%
Rural remote 15.7% 11.0 – 20.4%

Urban and rural accessible intervals do not overlap, so those two differ. Urban and rural remote overlap almost entirely, so those two do not differ detectably.

Now the trap: two intervals that overlap slightly can still be significantly different. Comparing intervals is a cruder test than comparing the difference, because the difference has its own standard error which is smaller than the sum of the two.

svyttest(I(displacement_status == "displaced") ~ I(stratum == "urban"),
         subset(design, stratum != "rural-remote"))
# The difference of two estimates, with its own standard error
diff = p_urban - p_rural
se_diff = math.sqrt(se_urban**2 + se_rural**2)
print(f"difference {diff:.1%}, 95% CI {diff - 1.99 * se_diff:.1%} to "
      f"{diff + 1.99 * se_diff:.1%}")

The rule: to compare two estimates, estimate the difference and put an interval on that. Reading two intervals is a screening step, not a conclusion.

Note also that the Python line above assumes the two estimates are independent, which is true for separate strata and false for two subgroups within a stratum — in that case the covariance term matters and svyttest or svycontrast handles it properly.

Rounding, and false precision

An interval of ±3.6 points does not support three decimal places.

print(f"{p:.1%} (95% CI {low:.1%} to {high:.1%})")
sprintf("%.1f%% (95%% CI %.1f-%.1f)", 100 * p, 100 * low, 100 * high)
  • One decimal place for a percentage from a survey of this size. Two implies a precision of 0.01 points, which is a hundred times narrower than your interval.
  • Round the interval outwards, never inwards. 25.47–32.71 becomes 25.4–32.8.
  • Never round the point estimate to a threshold. An estimate of 14.96% reported as “15%” beside a 15% emergency threshold has made a classification decision by rounding.

The sentence

The number, the interval, the denominator, the design. One sentence, and it is the deliverable of this whole course.

Household food insecurity was estimated at 29.1% (95% CI 25.5–32.7), from 996 households in 75 clusters, weighted to the sampling frame and adjusted for non-response; design effect 1.60, effective sample 623.

Compare that with what usually gets published:

Food insecurity affects 32.8% of households.

The second sentence is unweighted, has no interval, no denominator and no design. It is also 3.7 points higher, and it is the one that will be quoted.

Reading an interval against a threshold

The question this sector asks most often, and the one the interval exists for.

THRESHOLD = 0.15

if high < THRESHOLD:
    verdict = "below the threshold"
elif low > THRESHOLD:
    verdict = "above the threshold"
else:
    verdict = "cannot be distinguished from the threshold"
print(verdict)
dplyr::case_when(
  high < 0.15 ~ "below the threshold",
  low  > 0.15 ~ "above the threshold",
  TRUE        ~ "cannot be distinguished from the threshold"
)

The third branch is the one people delete, and it is the most common honest answer. An estimate of 14.2% with an interval of 11.0–18.1 does not say the situation is below the emergency threshold; it says the survey cannot tell. That sentence is unwelcome, correct, and considerably cheaper than a response scaled on a number that could not support the decision.

What comes next

The interval tells you what your whole sample supports. Cut the sample into districts, age groups and sexes and each piece supports far less. The last lesson is where to stop cutting, and how to say so in a table.

Teach this lesson

The lesson as a slide deck, with the prose kept in the speaker notes rather than on the slide. Generated from this page, so it cannot fall out of step with it.

Start the slideshowRead the slides

The PDF needs no software and projects from any machine. The PowerPoint file is there to be edited — add your organisation's branding, cut a section for a shorter session, or merge two lessons into a workshop.