When ‘Compliant’ Is a Legal Claim: Computing EUDR Deforestation Risk — and Knowing When to Abstain

The EU Deforestation Regulation (EUDR) went live for large operators at the end of 2025 and reaches smaller ones in mid-2026. The rule is deceptively simple to state: if you place coffee, cocoa, and a handful of other commodities on the EU market, you have to prove they were produced on land that was not deforested after 31 December 2020 — and you have to point at the specific plot of land where they were grown, by geolocation.

That last part is where software comes in, and where a lot of well-meaning systems quietly go wrong. A “compliant” determination isn’t a dashboard color. It’s a legal claim that rides along with a shipment. So the interesting engineering question isn’t how do I score deforestation risk from satellite data — that’s a solved-ish geospatial problem. The interesting question is: when should the system refuse to answer?

I built a small, runnable reference project to make both halves concrete. It’s deliberately tiny — pure Python standard library, no database required to run it — but it takes the hard part seriously.

The easy half: forest loss inside a boundary

Real deforestation baselines look nothing like a tidy vector map. The canonical source — the Hansen / UMD Global Forest Change dataset behind Global Forest Watch — is a raster: a grid of ~30 m pixels, one band telling you whether each pixel was forest in a baseline year, another band telling you the year that pixel lost forest cover. That shape matters, so I modeled it directly: a grid of cells, each carrying forest (1/0 at baseline) and loss_year (0 for “no loss”).

Given that, assessing a plot is a rasterization problem. You take the plot’s polygon, walk the grid cells whose centroids fall inside it (a plain point-in-polygon test, the in-process cousin of PostGIS’s ST_Intersects), and tally the cells that were forest at baseline but lost cover after the cut-off year:

risk_score = (post-cut-off loss area inside plot) / (plot area)

A plot fully inside a 2022 clearing scores ~1.0 — deforestation detected. A plot with a small 2021 patch in one corner scores a few percent — elevated, worth a look. A plot in untouched forest scores 0.

One subtlety that’s easy to get wrong: pre-cut-off loss is legal. A plot cleared in 2019 is fine under EUDR. If you naively flag “any forest loss ever,” you’ll generate a mountain of false positives on land that was legitimately converted years before the regulation existed. My checker counts only loss_year > 2020, and it records a note when it saw older loss and deliberately excluded it — because “we looked and chose not to count this” is a very different audit statement from “we didn’t look.”

The cut-off is a parameter, not a constant. One of my tests moves it to 2018 and watches a previously-clean plot flip to “deforestation detected.” Regulations change; the boundary date shouldn’t be buried in an if.

The half that actually matters: abstaining

Here’s the thing about a legal claim: a false “compliant” is far more expensive than a false “needs review.” If the system wrongly flags a clean plot, an analyst spends five minutes and clears it. If the system wrongly certifies a plot that was deforested — or certifies a plot whose boundary data was garbage — you’ve attached a false claim to a shipment, and the cost is regulatory exposure, reputational damage, and a broken chain of custody.

That asymmetry should be encoded in the software, not left to a human’s good judgment downstream. So the checker’s most important output isn’t a score — it’s a refusal. Before it computes any risk number, it runs a gate, and any of these routes the plot to MANUAL_REVIEW with a reason code instead of a determination:

The invariant I care about most is the one my test suite asserts explicitly: no plot under manual review can ever back a compliance claim. A MANUAL_REVIEW result carries a None risk score and a hard “no” on whether it may feed a determination. That’s not a nice-to-have; it’s the whole point.

Why this generalizes

I don’t work primarily in geospatial — this project was a way to think in the domain. But the shape is exactly what I do spend my time on: high-volume pipelines that ingest messy, heterogeneous inputs from sources you don’t control, and produce outputs that other systems (or regulators, or lenders, or auditors) are going to trust. The recurring lesson across every one of them is the same: the mature move is building the part that says “I can’t answer this cleanly, and here’s exactly why,” rather than manufacturing a confident number to fill a column.

Satellite signal in smallholder landscapes is genuinely unreliable — agroforestry, shade-grown plots, and fallow land are hard to distinguish from forest, and models trained on clear-cut deforestation underperform on mosaic terrain. That’s precisely why the abstain path isn’t a corner case. It’s the main road. Ground-truth field data is what makes the satellite signal trustworthy — and a system that knows the boundary of its own knowledge is what makes the whole thing defensible.