Reporting NVA and VVA Accuracy
TL;DR: NVA uses open-terrain checkpoints: report RMSEz and, where the specification asks for it, the legacy 95 % figure 1.96 × RMSEz. VVA uses vegetated checkpoints: report the 95th percentile of absolute errors. Compare both against the thresholds for the target quality level — for USGS QL2, RMSEz ≤ 10 cm, NVA ≤ 19.6 cm and VVA ≤ 30 cm — and state the checkpoint counts, datums and method alongside the numbers.
# Context and Motivation
This guide is part of Vertical Accuracy Assessment for LiDAR. Two statistics are needed because errors behave differently under vegetation. On open, hard ground, LiDAR errors are close to normally distributed, and RMSEz summarizes them well. Under canopy, the laser reaches the ground less often, ground classification is harder, and errors develop a long tail — a few checkpoints may be off by half a metre. A normal-distribution statistic would understate that tail, so vegetated accuracy is reported with a percentile that makes no distribution assumption.
Standards have evolved. The ASPRS Positional Accuracy Standards for Digital Geospatial Data, edition 2 (2023), report accuracy as RMSE and dropped the requirement to publish 95 % confidence values; the USGS Lidar Base Specification and many contracts still express thresholds as NVA and VVA. The practical approach is to compute everything and report what the governing specification asks for.
# Prerequisites and Assumptions
- Checkpoint results with ΔZ and a land-cover label, as produced in computing RMSEz against survey checkpoints.
- Datum issues resolved: the mean open-terrain error should be small relative to its standard deviation before reporting.
- The governing specification and target quality level.
- Python with pandas and NumPy.
# Step-by-Step Implementation
# Step 1 — Split by land cover
Use only open-terrain checkpoints for NVA and only vegetated checkpoints for VVA. Never pool them.
# Step 2 — Compute NVA
RMSEz of open ΔZ, and 1.96 × RMSEz if the specification uses the 95 % confidence form.
# Step 3 — Compute VVA
The 95th percentile of |ΔZ| for vegetated checkpoints. With few vegetated checkpoints, the percentile is effectively the second- or third-largest error — say so.
# Step 4 — Compare with thresholds
Look up the thresholds for the target quality level and record pass or fail for each statistic.
# Step 5 — Write the report table
Include counts, datums, geoid model, method (TIN of class 2 returns), statistics, thresholds and outcome. Keep the per-checkpoint table as an appendix.
# Complete Working Example
"""NVA/VVA report against USGS quality-level thresholds."""
from __future__ import annotations
import numpy as np
import pandas as pd
# Thresholds in metres (USGS Lidar Base Specification quality levels; confirm the edition).
QL = {
"QL0": {"rmsez": 0.050, "nva95": 0.098, "vva95": 0.147},
"QL1": {"rmsez": 0.100, "nva95": 0.196, "vva95": 0.300},
"QL2": {"rmsez": 0.100, "nva95": 0.196, "vva95": 0.300},
"QL3": {"rmsez": 0.200, "nva95": 0.392, "vva95": 0.588},
}
def report(res: pd.DataFrame, level: str = "QL2") -> pd.DataFrame:
ok = res[res.status == "ok"]
open_dz = ok.loc[ok.cover == "open", "dz"].to_numpy()
veg_dz = ok.loc[ok.cover == "vegetated", "dz"].to_numpy()
t = QL[level]
rmse = float(np.sqrt(np.mean(open_dz ** 2)))
nva95 = 1.96 * rmse
vva95 = float(np.percentile(np.abs(veg_dz), 95)) if veg_dz.size else np.nan
rows = [
("Open checkpoints (n)", len(open_dz), "", ""),
("Vegetated checkpoints (n)", len(veg_dz), "", ""),
("Mean error, open (m)", round(open_dz.mean(), 3), "", ""),
("RMSEz, open (m)", round(rmse, 3), t["rmsez"], rmse <= t["rmsez"]),
("NVA at 95 % (1.96 × RMSEz) (m)", round(nva95, 3), t["nva95"], nva95 <= t["nva95"]),
("VVA, 95th percentile |ΔZ| (m)", round(vva95, 3), t["vva95"], vva95 <= t["vva95"]),
]
table = pd.DataFrame(rows, columns=["Statistic", "Value", f"{level} threshold", "Pass"])
table["Pass"] = table["Pass"].map({True: "yes", False: "no", "": ""})
return table
if __name__ == "__main__":
res = pd.read_csv("qa/checkpoint_results.csv")
res["dz"] = res.z_lidar - res.z_check
tbl = report(res, "QL2")
print(tbl.to_markdown(index=False))
tbl.to_csv("qa/accuracy_report_ql2.csv", index=False)Rendered as Markdown, the table drops straight into a delivery report:
| Statistic | Value | QL2 threshold | Pass |
|:--------------------------------|-------:|--------------:|:-----|
| Open checkpoints (n) | 42 | | |
| Vegetated checkpoints (n) | 28 | | |
| Mean error, open (m) | 0.004 | | |
| RMSEz, open (m) | 0.052 | 0.1 | yes |
| NVA at 95 % (1.96 × RMSEz) (m) | 0.102 | 0.196 | yes |
| VVA, 95th percentile |ΔZ| (m) | 0.214 | 0.3 | yes |# Key Parameter Table
| Quality level | RMSEz (cm) | NVA 95 % (cm) | VVA 95th pct (cm) | Typical use |
|---|---|---|---|---|
| QL0 | ≤ 5.0 | ≤ 9.8 | ≤ 14.7 | Engineering, dense corridor work |
| QL1 | ≤ 10.0 | ≤ 19.6 | ≤ 30.0 | High-density regional mapping |
| QL2 | ≤ 10.0 | ≤ 19.6 | ≤ 30.0 | National programme baseline |
| QL3 | ≤ 20.0 | ≤ 39.2 | ≤ 58.8 | Older or lower-density collections |
These values follow the USGS Lidar Base Specification’s quality-level tables; check the edition your contract cites, because thresholds and wording are revised periodically.
# Verification
- Counts first. A VVA from eight checkpoints is the largest error in all but name. State counts in the report and flag statistics computed from fewer than 20 checkpoints.
- Reproduce from the appendix. Anyone should be able to recompute every number from the per-checkpoint table you ship. Test that by computing the report from the saved CSV, not from in-memory data.
- Consistency with relative accuracy. A project that passes NVA but shows large swath-to-swath differences probably has checkpoints concentrated in well-behaved areas; see measuring swath-to-swath relative accuracy.
# Gotchas and Edge Cases
1.96 assumes normal, unbiased errors. When mean error is not near zero, 1.96 × RMSEz is not a 95 % bound. That is one reason the newer ASPRS edition reports RMSE directly. Fix the bias, or report RMSE and bias separately.
Percentiles on small samples. NumPy’s default percentile interpolates between order statistics. With 20 vegetated checkpoints, the 95th percentile lies between the 19th and 20th largest values. Document the method (NumPy “linear”) so others reproduce the number.
Land cover labels. Label checkpoints in the field, not afterwards from imagery. A checkpoint called “open” that sits under a thin canopy silently inflates NVA.
Specification language. “Meets QL2” is a statement about the whole specification — density, classification, accuracy and more — not accuracy alone. Say “meets QL2 vertical accuracy requirements” unless every part has been checked.
# Frequently Asked Questions
What does NVA mean in LiDAR accuracy reporting?
Non-vegetated vertical accuracy: accuracy measured with checkpoints on open terrain such as bare ground, short grass and pavement. It is based on RMSEz and, in legacy reporting, expressed at 95 percent confidence as 1.96 times RMSEz.
What does VVA mean?
Vegetated vertical accuracy: accuracy measured with checkpoints under vegetation, reported as the 95th percentile of absolute vertical errors because those errors are not normally distributed.
What are the QL2 vertical accuracy thresholds?
Under the USGS Lidar Base Specification, QL2 requires RMSEz of 10 centimetres or better, NVA at 95 percent of 19.6 centimetres or better, and VVA of 30 centimetres or better. Confirm against the edition your project cites.
Do the current ASPRS standards still use NVA and VVA?
Edition 2 of the ASPRS Positional Accuracy Standards reports vertical accuracy as RMSE and no longer requires 95 percent confidence figures. Many specifications and contracts still use NVA and VVA, so compute both and report what your governing document requires.
# Related
- Vertical Accuracy Assessment for LiDAR — the end-to-end workflow
- Computing RMSEz Against Survey Checkpoints — the statistic behind NVA
- Measuring Swath-to-Swath Relative Accuracy — the other accuracy requirement
- Checking Pulse Spacing Against USGS Quality Levels — the density side of quality levels
- Understanding ASPRS Classification Codes — ground classification behind the checkpoints