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.
Biodiversity loss is reported in PDF*year and takes values around
1e-13. The cut-offs are of the same order:
eiod_lookup_cutoff("biodiv", group = 4, metric = "abs")
#> Table 3 absolute threshold - Biodiversity loss
#> unit: PDF*year per capita per day
#>
#> group value as printed
#> Male 10+ yr 1.2844358718e-12 1.2844358718E-12
#>
#> printed at 15 significant digits.
#> `as printed` is the literal text this package ships, which has
#> never been through a numeric type. Nothing is rescaled here.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.
x <- 3.6896027444e-13
identical(x, as.numeric("3.6896027444e-13"))
#> [1] TRUE
.Machine$double.xmin # smallest normal double
#> [1] 2.225074e-308The exponent is not the problem. Nothing in reiod uses a tolerance, and nothing should.
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 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.
grp <- eiod_age_sex_group(eiod_demo$age, eiod_demo$sex)
eiod_absolute_threshold(as.integer(eiod_demo$water_litres), grp, "water")
#> Error:
#> ! `value` is an integer vector, not a double. Integer storage truncates toward zero, so the fractional part of every impact is already gone before reiod sees it - and `is.numeric()` returns TRUE for an integer, so nothing else would catch this. On 200,000 simulated respondent-days the companion Stata implementation measured a 13% downward bias in mean greenhouse gas emissions from exactly this fault. Convert with as.double(x), then re-check. See eiod_check_impact().Everything else below is opt-in.
eiod_check_precision() answers two separate
questions.
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.
eiod_check_precision(eiod_demo$biodiv_pdf, name = "biodiv_pdf")
#> reiod data quality report
#>
#> ERROR 0
#> WARNING 0
#> NOTE 2
#>
#> NOTE biodiv_pdf precision up to 17 significant digits retained across 588 distinct values.
#> NOTE biodiv_pdf proximity cut-off proximity not checked: supply `group` and `indicator` to find
#> out whether precision could change any classification.
#>
#> No blocking problems.The same column after a single-precision round trip:
eiod_check_precision(signif(eiod_demo$biodiv_pdf, 7), name = "biodiv_float")
#> reiod data quality report
#>
#> ERROR 0
#> WARNING 1
#> NOTE 1
#>
#> WARNING biodiv_float precision at most 7 significant digits across 588 distinct values. Consistent with
#> single-precision storage or a truncated export. Check how the column was
#> written.
#> NOTE biodiv_float proximity cut-off proximity not checked: supply `group` and `indicator` to find
#> out whether precision could change any classification.
#>
#> No blocking problems.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.
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:
eiod_check_precision(eiod_demo$biodiv_pdf, grp, "biodiv", name = "biodiv_pdf")
#> reiod data quality report
#>
#> ERROR 0
#> WARNING 0
#> NOTE 2
#>
#> NOTE biodiv_pdf precision up to 17 significant digits retained across 588 distinct values.
#> NOTE biodiv_pdf proximity no observation sits within 1e-06 of the absolute threshold, so stored
#> precision does not change any classification here.
#>
#> No blocking problems.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:
eiod_check_precision(eiod_demo$biodiv_pdf, grp, "biodiv", name = "biodiv_pdf",
metric = "rel", percentile = 30)
#> reiod data quality report
#>
#> ERROR 0
#> WARNING 0
#> NOTE 2
#>
#> NOTE biodiv_pdf precision up to 17 significant digits retained across 588 distinct values.
#> NOTE biodiv_pdf proximity no observation sits within 1e-06 of the P30 benchmark, so stored
#> precision does not change any classification here.
#>
#> No blocking problems.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?”.
eiod_check_precision(eiod_demo$biodiv_pdf, grp, "biodiv", name = "biodiv_pdf",
tolerance = 1e-3)
#> reiod data quality report
#>
#> ERROR 0
#> WARNING 1
#> NOTE 1
#>
#> WARNING biodiv_pdf proximity 1 observation(s) sit within 1e-03 (relative) of the absolute threshold.
#> Their classification is not robust to the precision in your input. Rows:
#> 55
#> NOTE biodiv_pdf precision up to 17 significant digits retained across 588 distinct values.
#>
#> No blocking problems.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.
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.
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:
eiod_lookup_cutoff("biodiv", metric = "abs")
#> Table 3 absolute threshold - Biodiversity loss
#> unit: PDF*year per capita per day
#>
#> group value as printed
#> Male 0-1.9 yr 3.6896027444e-13 3.6896027444E-13
#> Male 2-4.9 yr 5.7244838747e-13 5.7244838747E-13
#> Male 5-9.9 yr 7.8836516181e-13 7.8836516181E-13
#> Male 10+ yr 1.2844358718e-12 1.2844358718E-12
#> Female 0-1.9 yr 3.5749209701e-13 3.5749209701E-13
#> Female 2-4.9 yr 5.3018709712e-13 5.3018709712E-13
#> Female 5-9.9 yr 7.2382064564e-13 7.2382064564E-13
#> Female 10+ yr 1.033173291e-12 1.0331732910E-12
#>
#> printed at 15 significant digits.
#> `as printed` is the literal text this package ships, which has
#> never been through a numeric type. Nothing is rescaled here.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.)
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.