Repository navigation
Corrected annual series: per-pixel run-length filter that keeps a sustained switch and overwrites flicker years #64
Description
Context
#62 ran dft_rast_break_class() on the published seven-year IO LULC series of every group that has one (bulk_co_ff04, necr_ch_ff04, lnth_ch_ff04, kotl_bt_ff04). The #9 body named a "corrected series" — a per-pixel mode filter that keeps a sustained break and overwrites the flicker years — as worth building only if flicker turned out large everywhere. It did.
The numbers (data-raw/logs/break_class_groups/summary_groups.csv, one row per group)
| group | changed_ha | pct_sustained | pct_endpoint | pct_flicker | stable_flicker_ha | pct_stable_flicker_of_valid |
|---|---|---|---|---|---|---|
| bulk | 4625.0 | 19.7 | 36.4 | 44.0 | 3186.5 | 7.76 |
| necr | 5779.4 | 31.0 | 29.4 | 39.6 | 3828.3 | 9.15 |
| lnth | 1629.7 | 20.6 | 31.0 | 48.5 | 1576.7 | 9.85 |
| kotl | 3537.8 | 24.7 | 33.1 | 42.3 | 2235.5 | 3.22 |
Flicker is the largest category of the 2017 -> 2023 changed area in every group (39.6-48.5%), and a further 3.2-9.9% of every floodplain flickers while reading stable on the endpoints — 0.63 to 0.97 times the changed area itself (stable_flicker_over_changed), invisible to the two-epoch layer. Full reading in inst/notes/temporal-qa-groups.md.
What to build
A function that takes the same named list of classified years dft_rast_break_class() takes and returns a corrected series (a named list, same grid) in which:
- a pixel with
n_flips == 0is unchanged; - a pixel with
n_flips == 1is unchanged — a clean switch is real by this leg's evidence, whatever itsn_before/n_after(the caller thresholds, as everywhere else in the package); - a pixel with
n_flips >= 2is overwritten year by year with a mode that respects a sustained switch: not the whole-series mode (dft_rast_consensus()already does that and votes a real mid-window switch back to its old class), but a run-length rule — a run of at leastrun_minconsecutive years of one class is kept, and any run shorter than that is relabelled to the neighbouring long run (the earlier one on a tie, or by a documented rule).
The two-epoch transition of the corrected series is then the deliverable a report can quote as change: dft_rast_transition(corrected, from = "2017", to = "2023"). Its changed area should land near the pct_sustained + pct_endpoint share of today's, and the note's Q1 table is the acceptance check — run the corrected series through dft_rast_break_class() again and the flicker row should be near zero.
Open decisions for the plan gate
run_mindefault: 2 matches the "sustained two years each side" reading used throughout Categorical breakpoint detection: sustained switch vs flicker across the annual class series (the temporal leg of change QA) #9 / Temporal QA across five watershed groups from the published annual series: does the BULK split generalise? #62.- What to do with a flicker pixel that has no run of length
run_minat all (e.g.ABABABA): leave as is and flag, or mode. Either is defensible; the count of such pixels is the number to report. - Whether the endpoint-only breaks (
n_before == 1orn_after == 1, 29-36% of changed area) are touched. Temporal QA across five watershed groups from the published annual series: does the BULK split generalise? #62's Q2 says 2017 is the odd endpoint somewhat more often than 2023 in every group (ratio 1.09-1.25) and argues against moving the baseline; this function should leave clean switches alone and let the caller decide.
Relates to #9, #62. Scale-test on the BULK pair before the PR per CLAUDE.md; the four groups' COGs are one data-raw/break_class_groups.R <group> away.
Activity
NewGraphEnvironment commentedon Sep 7, 2026
#66's article is up in #71 and states the case
this issue builds on, for a reader outside the package:
What a Land-Cover Change Figure Is Made Of
(live once #71 merges and pkgdown deploys).
The number that matters here: unsettled area is the largest of the three categories in every one
of the four groups — 39.6% to 48.5% of endpoint-changed area — which is the case for building the
run-length filter this issue describes.
One thing #66 surfaced that is worth carrying into the design.
cat_fun()'s category 3 is everypixel with
n_flips >= 2whether or not the endpoints differ, so it pools two populations: onBULK, 2,032.9 ha that changed and never settled, and 3,186.5 ha that flickers while reading
identical in 2017 and 2023. A corrected series has to treat those separately — the second group is
invisible to a two-epoch comparison entirely, and is 3.2-9.9% of floodplain area across the four
groups. Per-transition-class composition is now committed in
inst/extdata/temporal-composition/summary_class_temporal.csvif it is useful for choosingrun_min.Record the cross-links in the #66 archive