Skip to contents

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:

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-308

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.

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.

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.

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.

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:

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.

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:

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.)

See also