Eutrophication: a corrected cut-off, and why it matters

What you need to know

Supply eutrophication in grams PO₄³⁻eq per person per day, exactly as the EIOF database gives it to you. Nothing needs converting.

That is the whole instruction. The rest of this page explains why it needs saying at all, and why eight values in eiod_cutoffs deliberately do not match the currently published methods paper.

The eight corrected thresholds

subset(eiod_cutoffs, value_source != "published",
       c(demogroup_label, metric, unit, value, value_source))
#>    demogroup_label metric        unit     value              value_source
#> 9    Male 0-1.9 yr    abs g PO4(3-)eq  7.682720 intake correction 2026-10
#> 10   Male 2-4.9 yr    abs g PO4(3-)eq 11.919876 intake correction 2026-10
#> 11   Male 5-9.9 yr    abs g PO4(3-)eq 16.415830 intake correction 2026-10
#> 12     Male 10+ yr    abs g PO4(3-)eq 26.745323 intake correction 2026-10
#> 13 Female 0-1.9 yr    abs g PO4(3-)eq  7.443923 intake correction 2026-10
#> 14 Female 2-4.9 yr    abs g PO4(3-)eq 11.039886 intake correction 2026-10
#> 15 Female 5-9.9 yr    abs g PO4(3-)eq 15.071844 intake correction 2026-10
#> 16   Female 10+ yr    abs g PO4(3-)eq 21.513377 intake correction 2026-10

These are the Table 3 eutrophication thresholds. Intake supplied them by email in October 2026. The published PDF prints the same eight cells in kilograms; Table 4f and the EIOF food databases use grams. Intake confirmed that both tables are intended to be in grams, and corrected Table 3.

Every other row in the dataset — all 608 of them — is exactly what the paper prints.

The quantity did not change, only the unit

This is the reassuring part. The corrected values are the printed ones multiplied by 1000:

corrected <- subset(eiod_cutoffs, value_source != "published")
corrected <- corrected[order(corrected$demogroup), ]

published_in_pdf <- c(0.0076827204, 0.0119198764, 0.0164158298, 0.0267453228,
                      0.0074439228, 0.0110398856, 0.0150718438, 0.0215133770)

data.frame(
  group        = corrected$demogroup_label,
  pdf_kg       = published_in_pdf,
  shipped_g    = corrected$value,
  pdf_x1000    = published_in_pdf * 1000,
  agrees       = sprintf("%.10f", corrected$value / 1000) ==
                 sprintf("%.10f", published_in_pdf)
)
#>             group      pdf_kg shipped_g pdf_x1000 agrees
#> 1   Male 0-1.9 yr 0.007682720  7.682720  7.682720   TRUE
#> 2   Male 2-4.9 yr 0.011919876 11.919876 11.919876   TRUE
#> 3   Male 5-9.9 yr 0.016415830 16.415830 16.415830   TRUE
#> 4     Male 10+ yr 0.026745323 26.745323 26.745323   TRUE
#> 5 Female 0-1.9 yr 0.007443923  7.443923  7.443923   TRUE
#> 6 Female 2-4.9 yr 0.011039886 11.039886 11.039886   TRUE
#> 7 Female 5-9.9 yr 0.015071844 15.071844 15.071844   TRUE
#> 8   Female 10+ yr 0.021513377 21.513377 21.513377   TRUE

Divide any shipped value by 1000 and you recover the printed figure to the ten decimals the PDF gives. data-raw/build_data.R asserts this on every build, so the two can never silently part company.

No classification moves. Tested across 400,000 simulated gram-scale values, the old behaviour (divide the user’s grams by 1000, compare against the PDF’s kilograms) and the new one (compare grams directly) agreed everywhere. They can differ only within about one unit in the last place of a cut-off — and there the new path is the more exact of the two, because it no longer divides before comparing.

What this means for your code

grp    <- eiod_age_sex_group(eiod_demo$age, eiod_demo$sex)
eutr_g <- eiod_demo$eutroph_gpo4e   # already grams - nothing to convert

Both metrics now take the same input, with no conversion and no message:

table(eiod_absolute_threshold(eutr_g, grp, "eutroph"), useNA = "ifany")
#> 
#>   0   1 
#> 279 321
table(eiod_relative_band(eutr_g, grp, "eutroph"), useNA = "ifany")
#> 
#>   1   2   3   4   5   6   9  10 
#> 210   3  47  38  66  57  62 117

If you wrote a script against an earlier version of this package, remove any /1000 you applied to eutrophication. Between July and October 2026 reiod expected kilograms for the absolute metric and divided for you; it no longer does. Dividing now would make your results wrong by a factor of a thousand in the other direction.

eiod_check_magnitude() catches that: a eutrophication column a thousand times too small is reported as a likely unit error.

How the problem was found

Worth recording, because the same reasoning applies to any reference table that ships without units.

The paper states no unit for Tables 4a–4g anywhere. Taken at face value, the Table 4f benchmarks were about 690 times the Table 3 threshold for the same demographic group — a gap no other indicator came close to.

The decisive test was that the ratio between the two tables is constant across all eight demographic groups. A methodological difference would vary by group; a unit difference cannot. Comparing that ratio across indicators put eutrophication three orders of magnitude outside the range of the other six:

Indicator Table 4 P10 ÷ Table 3 threshold
Pasture land use 0.026
Total land use 0.285
Water use 0.346
Biodiversity loss 0.548
Arable land use 0.688
Greenhouse gas emissions 1.449
Eutrophication (as published) 689.647
Eutrophication ÷ 1000 0.690

Divided by 1000 it falls back among the others, almost exactly on the arable-land value. Supporting evidence came from the EIOF country databases, which report Eutr. (g PO43-e/kg) while every other column matches the paper’s unit, and from the fact that the kilogram reading implies a 10th-percentile adult diet of 18.4 kg PO₄³⁻eq per day.

That analysis was put to Intake as a technical note, and the correction above is their reply.

Checking the current state yourself

Every indicator now uses one unit across both tables:

with(eiod_cutoffs, table(indicator, unit))
#>              unit
#> indicator     g PO4(3-)eq kg CO2eq litres m2*year PDF*year
#>   biodiv                0        0      0       0       88
#>   eutroph              88        0      0       0        0
#>   ghg                   0       88      0       0        0
#>   landarable            0        0      0      88        0
#>   landpasture           0        0      0      88        0
#>   landtotal             0        0      0      88        0
#>   water                 0        0     88       0        0

And the Table 3 threshold sits in a sensible place within its own reference distribution, for every indicator:

for (ind in eiod_indicators$indicator) {
  thr <- as.numeric(eiod_lookup_cutoff(ind, 4, "abs"))
  p   <- vapply(seq(10, 90, 10),
                function(q) as.numeric(eiod_lookup_cutoff(ind, 4, "rel", q)),
                numeric(1))
  k <- sum(thr >= p)
  pos <- if (k == 0) "below P10" else if (k == 9) "at/above P90" else
    sprintf("P%d-P%d", k * 10, (k + 1) * 10)
  cat(sprintf("%-12s %s\n", ind, pos))
}
#> landtotal    P40-P50
#> landarable   P60-P70
#> landpasture  P40-P50
#> ghg          below P10
#> eutroph      P40-P50
#> water        at/above P90
#> biodiv       P60-P70

Eutrophication now sits at P40–P50 with the two land-use indicators — where, before the correction, reading it at face value put it below P10 alongside greenhouse gases.

See also

  • ?eiod_cutoffs — the reference data and the value_source column
  • ?eiod_list_indicators — the input unit for all seven
  • vignette("precision") — the pre-flight checks