--- title: "Check precision before you compute anything" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Check precision before you compute anything} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(reiod) ``` This is a pre-flight check. Run it **before** you generate any indicator, not after a result looks odd — by then the damage is indistinguishable from a finding. It has its own vignette because it is the question people ask first about this package, and because the honest answer has two halves that are easy to confuse: the thing most people worry about is harmless, and the thing that actually destroys impact data is invisible. ## The worry, and why it is the wrong one Biodiversity loss is reported in PDF\*year and takes values around `1e-13`. The cut-offs are of the same order: ```{r} eiod_lookup_cutoff("biodiv", group = 4, metric = "abs") ``` It is reasonable to look at that and wonder whether a comparison at such a small magnitude can be trusted — whether some epsilon is needed, whether the digits survive. **They do.** R's `numeric` is an IEEE 754 double: roughly 15 to 17 significant decimal digits, with an exponent range to about `1e±308`. A value of `3.69e-13` sits nowhere near the edge of that range, and `<` on two doubles is exact. ```{r} x <- 3.6896027444e-13 identical(x, as.numeric("3.6896027444e-13")) .Machine$double.xmin # smallest normal double ``` The exponent is not the problem. Nothing in reiod uses a tolerance, and nothing should. ## What actually destroys impact data Precision is lost **before R sees the number**, in how the column was written and read. Four mechanisms, in rough order of how badly they bite: | Mechanism | What it does | Caught by | |---|---|---| | **Factor** column | `as.numeric()` returns level codes. `3.7e-13` becomes `1`. | error, always | | **Integer** column | Truncates toward zero. Everything below 1 becomes `0`. | error, always | | **Character** column | One stray cell such as `"<0.001"` retypes the whole column. | error, always | | **Truncated export** | Excel writes what it displays; a `%.6g` CSV format drops digits. | `eiod_check_precision()` | | **Underflow** | Values below `1e-308` flush to subnormal or to zero. | `eiod_check_precision()` | The first three are *type* faults and the metric functions will not proceed past them. The last two leave a perfectly valid double column — just one with fewer digits in it than you think — and **nothing stops you**. ### The one that was measured The companion Stata implementation of these metrics was tested on 200,000 simulated respondent-days to find out which of these worries is real. The results are worth stating because they settle the question rather than hedging it: | Fault | Effect on the metric | |---|---| | Single-precision storage (~7 significant digits) | **0 of 200,000** absolute classifications changed; **1 of 200,000** relative bands changed | | Integer storage of greenhouse gas emissions | mean biased **3.836 → 3.336, a 13% downward bias**, with no warning | | Genuine underflow to zero | every affected diet classified as below every threshold | So the representation worry is empirically close to nothing, and the truncation worry is large. That is the opposite of the intuition most people arrive with, and it is why the integer check is the one test reiod performs automatically. ```{r, error = TRUE} grp <- eiod_age_sex_group(eiod_demo$age, eiod_demo$sex) eiod_absolute_threshold(as.integer(eiod_demo$water_litres), grp, "water") ``` Everything else below is opt-in. ## The two checks `eiod_check_precision()` answers two separate questions. ### 1. How many digits does the column actually retain? For each distinct value it finds the smallest number of significant digits that reproduces it exactly, and reports the maximum. Full-precision doubles return 17; a single-precision round trip returns exactly 7; an Excel three-digit re-save returns 3. The cases separate cleanly. ```{r} eiod_check_precision(eiod_demo$biodiv_pdf, name = "biodiv_pdf") ``` The same column after a single-precision round trip: ```{r} eiod_check_precision(signif(eiod_demo$biodiv_pdf, 7), name = "biodiv_float") ``` A maximum of 7 across hundreds of distinct values is not a coincidence. It is the signature of a column that passed through a 32-bit float at some point — Stata `float` storage, a database `REAL` column, or an export written with a narrow format. ### 2. Does it matter *here*? This is the question that actually decides whether to act, and it is the reason not to treat a precision warning as a verdict. Supply `group` and `indicator` and the check counts how many observations sit close enough to their own cut-off that rounding could move them across it: ```{r} eiod_check_precision(eiod_demo$biodiv_pdf, grp, "biodiv", name = "biodiv_pdf") ``` Usually the answer is zero and the worry can be set aside with a number attached rather than a shrug. When it is not zero you get the row numbers, and you can look at them. The same check against a relative benchmark rather than the absolute threshold: ```{r} eiod_check_precision(eiod_demo$biodiv_pdf, grp, "biodiv", name = "biodiv_pdf", metric = "rel", percentile = 30) ``` Tighten `tolerance` to ask a stricter question. The default `1e-6` is far wider than double precision — it is asking "could a *plausible export error* flip this?", not "could the last bit flip this?". ```{r} eiod_check_precision(eiod_demo$biodiv_pdf, grp, "biodiv", name = "biodiv_pdf", tolerance = 1e-3) ``` ## The recommended pre-flight block Put this above the code that generates your indicators, and put the `stop()` in writing. A check you can skip is a check that gets skipped on the day you are in a hurry. ```r library(reiod) # 1. state which column holds which indicator, once my_map <- c(ghg = "ghg_total_kg", biodiv = "pdf_year", water = "water_l") # 2. demographic group grp <- eiod_age_sex_group(dat$age, dat$sex) # 3. the full check suite: type, sign, zeros, unit magnitude, precision report <- eiod_check_data(dat, impacts = my_map, age = "age", sex = "sex", gdqs = "gdqs") report if (!summary(report)$pass) stop("fix the errors above before computing") # 4. precision, per indicator, against the cut-off that will actually be used for (ind in names(my_map)) { print(eiod_check_precision(dat[[my_map[[ind]]]], grp, ind, name = my_map[[ind]])) } # 5. only now generate the indicators abs_all <- eiod_absolute_all(dat, grp, my_map) band_all <- eiod_relative_all(dat, grp, my_map) ``` `eiod_check_data()` already runs the precision check for every indicator in `impacts`, so step 4 is for when you want to vary `metric`, `percentile` or `tolerance` — for example to check against the P30 benchmark that HDLEI uses, rather than against the absolute threshold. ## What the check cannot do **It cannot repair anything.** Digits that were never read cannot be recovered; no amount of re-typing in R puts them back. The fix is always upstream: re-export the source with full precision, or accept the loss having measured it. **It cannot tell you your values are correct.** It tests type, sign, magnitude and precision. If the upstream matching of food items to impact factors was wrong, every check here passes and the results are still wrong. These checks narrow the space of silent failures; they do not eliminate it. **It cannot see incomplete EIOF coverage.** Not every food has an impact factor, and a food without one contributes nothing to the day's total — so the total is understated, invisibly. By the time `reiod` sees one number per respondent-day, whether it was summed over complete or incomplete coverage is not recoverable from the number. Count it upstream and report it; see `vignette("troubleshooting")`, "The one you have to handle yourself". **It is not a substitute for looking at the cut-offs.** If you want to know what a respondent was compared against, ask: ```{r} eiod_lookup_cutoff("biodiv", metric = "abs") ``` The `as printed` column is the literal text transcribed from the paper. It has never been through a numeric type, so it is the thing to compare against if you need an exact decimal check — not a rendering of a double. (A `double` cannot hold most decimal fractions exactly. That is true of every statistical package. What is guaranteed here is that the stored value is the nearest double to the published decimal, that it round-trips back to it, and that the decimal itself ships as text in `eiod_cutoffs$value_published_str`.) ## See also * `vignette("troubleshooting")` — a broken dataset with five planted faults, including the integer one, worked through end to end. * `?eiod_check_precision`, `?eiod_check_data`, `?eiod_cutoffs`.