Skip to content

Validation

How accurate is a map, and how much area really changed? Draw a stratified random sample of the map, label it by eye, and get the error matrix, the accuracies and the error-adjusted area of each class with their confidence intervals, following the good practices of Olofsson et al. (2014).

The three functions work on any map: classes from classify, ai.predict, twdtw or som, a raster made elsewhere, or a map of events from extract_events or agreement (stratified as "no change" and "change"). Areas come from each pixel's size, in hectares (in a geographic CRS, the area of each row on the sphere); dask maps are read lazily, a row strip at a time. Tutorial: Accuracy & Area.

events = zeit.extract_events(zeit.landtrendr(ndvi))
design = zeit.sampling_design(events, expected_ua={"change": 0.7, "no change": 0.95})
points = zeit.stratified_sample(events, design=design)
s = zeit.interpret(cube, points, map=events, save="reference.gpkg")   # label each point by eye
acc = zeit.accuracy(events, s)
print(acc)                                 # overall, user's, producer's, mapped and estimated areas

sampling_design

zeit.sampling_design(
    data, expected_ua=0.75, std_error=0.01, n=None,
    alloc="proportional", min_per_stratum=50, bins=None,
    nodata="auto",
)

How many points the map needs to reach a target standard error of the overall accuracy (Olofsson et al. 2014, eq. 13), and how many in each stratum. The strata are the map's classes. Also exported as zeit.sampling_design.

Parameter Type Default Description
data DataArray, Dataset or path required The map to assess: classes (integers; names from its flag_meanings), or a map of events.
expected_ua float or dict 0.75 The user's accuracy you expect of each stratum: one value, or stratum name (or code) → value. Hard classes (0.6) need more points than easy ones (0.95).
std_error float 0.01 Target standard error of the overall accuracy.
n int None Total number of points; computed from expected_ua and std_error when None.
alloc str or dict "proportional" "proportional" (to the area of each stratum), "equal", "neyman" (to area × expected standard deviation), or stratum → number of points.
min_per_stratum int 50 Strata that would get fewer points get this many, taken from the larger ones, so the total stays n. Olofsson et al. recommend 50–100 for rare classes such as change. 0 disables.
bins sequence of float None Strata as intervals [a, b) of the map's values. For a map of events, periods of yod ([2000, 2010, 2020]), next to "no change".
nodata "auto", float or None "auto" Value outside every stratum: the map's NoData, a number, or none. A map of events keeps its 0 as "no change".

Returns a pandas.DataFrame with one row per stratum (indexed by its code): name, pixels, area, weight (share of the area), expected_ua and n. attrs holds the total n, the std_error, how points were allocated and the area_unit ("ha", or "pixels" without a CRS).

design = zeit.sampling_design(classes, expected_ua={"forest": 0.9, "pasture": 0.85, "deforestation": 0.6})
design.attrs["n"]                                           # e.g. 641
zeit.sampling_design(events, n=300, bins=[2000, 2010, 2020])  # no change, 2000-2009, 2010-2019

stratified_sample

zeit.stratified_sample(
    data, n=None, design=None, bins=None, nodata="auto", seed=42,
    **design_kwargs,
)

Draws the points: a simple random sample of pixels within each stratum, without replacement. The same map and seed give the same points in memory or lazily, whatever the chunks. Also exported as zeit.stratified_sample.

Parameter Type Default Description
data DataArray, Dataset or path required The map, as for sampling_design.
n int None Total number of points, allocated as sampling_design does.
design DataFrame None The table of sampling_design; its n column is used as is.
bins, nodata None, "auto" As for sampling_design.
seed int 42 Seed of the draw.
design_kwargs Passed to sampling_design when there is no design (expected_ua, alloc, min_per_stratum...).

Returns a geopandas.GeoDataFrame of points at the centres of the sampled pixels, in the map's CRS, with stratum (code), stratum_name, row and col. Keep the stratum column: accuracy weighs the points by it.

interpret

zeit.interpret(
    data, samples=None, classes=None, save=None, rgb=None, band=None,
    series=None, chips="year", chip_size=33, fit=None, map=None,
    strata=None, blind=True, interpreter=None, basemap=None, zoom=120,
    height=480, block=None, open=True,
)

Labels the reference points by eye, one after the other, in the viewer of zeit.plot with another layout: the queue of points and a form on the right, the map centred on the current point, a strip of image chips around it (one per year, the median of its dates), and the point's series below (a click on it dates the change). Each label is written to save at once; calling interpret again with the same file resumes where it stopped. In a notebook the viewer is shown right away and the returned object follows it live; in a script it opens in its own window and returns when the window is closed. Also exported as zeit.interpret.

Parameter Type Default Description
data DataArray or path required The imagery: a (time, band, y, x) cube (rgb picks the image's bands), or a (time, y, x) index.
samples GeoDataFrame or path None The points, e.g. from stratified_sample. Not needed to resume from save.
classes list of str None The reference classes; keys 1–9 pick them. Default: the classes of map.
save path None .gpkg, .geojson or .parquet written after every label; an existing file is resumed.
rgb three band names None The bands of the image, e.g. ["swir1", "nir", "red"] (default: red, green, blue).
band band name None One band to show instead of an RGB image.
series band name(s) or DataArray None What the chart shows: bands of data (e.g. "nbr"), or a (time, y, x) cube of the same area. Default: the image's bands.
chips "year", "month", "date", int or None "year" One chip per year (the median of its dates), month, date, every n images, or none. A click on a chip shows that image.
chip_size int 33 Side of a chip, in pixels.
fit result Dataset None A result of zeit drawn over the series, as in zeit.plot (landtrendr, ccdc, extract_events...).
map DataArray, Dataset or path None The map being assessed. Its value at the point is shown, and the Review tab gives its accuracy.
strata DataArray, Dataset or path None The map the points were stratified by, when it is not map (see accuracy).
blind bool True Hide what map and fit say at a point until it is labelled, and the review until every point is: the reference should not be swayed by the map.
interpreter str None Name recorded with each label (default: the login name).
basemap str None A web map under the data, as in zeit.plot.
zoom int 120 How many pixels across the map shows around a point.
height int 480 Height of the map in a notebook, in pixels.
block, open None, True Outside a notebook, as in zeit.plot: wait for the window to close (default: yes in a script); "native", "browser", or False to only serve the page.

Returns an Interpretation:

Attribute Meaning
samples The points with ref, ref_date, confidence (high, medium, low), note, status (todo, done, skipped), interpreter and labelled_at.
progress Points done, skipped and in the queue (total).
accuracy(map=None, **kwargs) accuracy of map (default: the map given) from the labelled points. zeit.accuracy(map, interpretation) works too.
save(path=None), close() Write the labels; close the window.
Key Action
1–9 Class
Enter Save and go to the next point without a label
s Skip the point (cloudy, no imagery...)
n / p Next / previous point
d Date of the change = the image shown
h / m / l Confidence high / medium / low
← →, Space Previous / next image, play

A click on the series sets the date of the change; a click on a point of the map goes to it. The Review tab (with map) shows the overall, user's and producer's accuracies and the areas, with their intervals, and the error matrix in points: a click on a cell (say, map "change", reference "no change") lists its points to look at again.

points = zeit.stratified_sample(loss, n=300)
s = zeit.interpret(landsat, points, rgb=["swir1", "nir", "red"], series="nbr", map=loss,
                   fit=lt, save="reference.gpkg")
acc = s.accuracy(date_tolerance=1)

accuracy

zeit.accuracy(
    map, samples, reference="ref", strata=None, bins=None,
    date_tolerance=None, reference_date="ref_date", confidence=0.95,
    nodata="auto",
)

The map's error matrix in proportions of area, its overall, user's and producer's accuracies and the error-adjusted area of each class, each with a confidence interval. The estimators are Stehman's (2014) for stratified random sampling; with the map's classes as strata they are Olofsson et al. (2014), eqs. 4–11, checked against the paper's example and against R's sits_accuracy. Also exported as zeit.accuracy.

Parameter Type Default Description
map DataArray, Dataset or path required The map to assess (classes, or events).
samples GeoDataFrame, path or zeit.interpret session required The reference points, in any CRS. Points without a reference label are left out (and counted).
reference str "ref" Column with the reference class: names of the map's classes, their codes, or for a map of events "change"/"no change" (or True/False, 1/0). A class the map never gives is kept (its user's accuracy is NaN, its area estimated).
strata DataArray, Dataset or path None The map the sample was stratified by, when it is not map: a sample of one map assesses another (Stehman 2014). Default: map's classes.
bins sequence of float None Classes of map as intervals of its values, as in sampling_design.
date_tolerance float None Maps of events: also compare dates. Of the points both call change, the share whose years (of the map's date and reference_date) are at most this far apart.
reference_date str "ref_date" Column with the reference date of the change.
confidence float 0.95 Level of the confidence intervals.
nodata "auto", float or None "auto" As for sampling_design.

Returns an Accuracy object; print(acc) gives a summary.

Attribute Meaning
overall estimate, se, ci_low, ci_high of the overall accuracy.
users, producers The same, per class (rows).
area Per class: mapped (area the map gives it), estimate (error-adjusted area), se, ci (half-width of the interval), ci_low, ci_high, in area_unit.
confusion Error matrix in proportions of area: map classes in rows, reference classes in columns.
counts The error matrix in points.
date With date_tolerance: within (estimated share of the change points whose dates agree), within_se, mean_difference (years), n.
n, n_unlabelled Points used, and points left out for having no label.
to_dataframe() User's, producer's and area in one table.
acc = zeit.accuracy(events, points, date_tolerance=1)
acc.area.loc["change", ["estimate", "ci"]]         # hectares of change, ± the 95% interval
acc.users.loc["change"]                            # how often a mapped change is real
acc.producers.loc["change"]                        # how much of the real change the map finds

# The same reference points assess another algorithm's map, weighed by the first map's strata
acc_ccdc = zeit.accuracy(zeit.extract_events(segments, band="nir", event_type="loss"), points, strata=events)

Points chosen by hand are not a sample

The estimates assume the points were drawn at random within the strata (or across the whole map). Field points or points picked on the screen give an error matrix, but not unbiased accuracies or areas. accuracy warns when the points have no stratum column and weighs them as if they were stratified by the map.