agentleFS
esmailhussienesmailhussien/terraproof/skills/time-series-and-change/SKILL.md

time-series-and-change

Skill0 starsChanged 25 days agoMITView on GitHub

Build analysis-ready satellite time series and detect change defensibly, covering per-observation masking and decoding, compositing, gap handling, temporal alignment, per-date index versus index of a composite, seasonality and phenology confounds, thresholded differences, trend and breakpoint methods, post-classification comparison and validation of change with reference data; use for multi-date optical or SAR monitoring, composites and change maps.

The file

We split this skill into the rules it gives an agent

8 rules. About 0 are common, 8 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈8

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Check whether your loader already decoded the data (STAC and cloud access).

    line 34
  2. Distinctive:

    Put all dates on one grid (CRS, transform, shape).

    line 36
  3. Distinctive:

    Keep one consistent product definition: processing level and baseline, sensor, band (B08 vs B8A) and, for SAR, quantity/scale and relative …

    line 40
  4. Distinctive:

    Keep per-observation provenance: item id, acquisition time (UTC), processing version and mask policy.

    line 42
  5. Distinctive:

    use a multi-year baseline per period (e.g.

    line 123
  6. Distinctive:

    use models that represent seasonality explicitly (harmonic or seasonal terms), as BFAST and CCDC do [S5, S6].

    line 125
  7. Distinctive:

    Use the intersection of both dates' valid masks.

    line 139
  8. Distinctive:

    No universal threshold exists.

    line 140

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Time series and change detection

Define the change question first

State what change means (land-cover conversion, disturbance, flooding, gradual trend), the minimum detectable size and magnitude, the period and the observation frequency needed. Different change-detection procedures produce different change maps even in the same environment [S1]; the question decides the method, not the other way round. Reviews organise Landsat time-series change detection by temporal frequency, preprocessing, algorithm and application [S2].

Related skills: satellite preprocessing (decoding, QA masks, single-period composites), spectral indices (formulas, dNBR), SAR basics (backscatter scales, orbit consistency), STAC and cloud access (search, loading, provenance), raster processing (grids, resampling), accuracy assessment (validation). Shared rules: engineering, simplicity, security.

Build an analysis-ready time series

Process each observation before stacking or compositing:

  1. Decode scale/offset exactly once and mask fill, cloud, shadow and other QA classes per sensor (satellite preprocessing). Check whether your loader already decoded the data (STAC and cloud access).
  2. Put all dates on one grid (CRS, transform, shape); categorical QA layers with nearest neighbour.
  3. Merge tiles of the same acquisition into one observation (e.g. odc-stac groupby="solar_day") so overlaps are not counted as extra dates [S3].
  4. Keep one consistent product definition: processing level and baseline, sensor, band (B08 vs B8A) and, for SAR, quantity/scale and relative orbit.
  5. Keep per-observation provenance: item id, acquisition time (UTC), processing version and mask policy.

Store a valid-observation count per pixel and period beside every composite or statistic. A median of two valid observations is not comparable with a median of twenty.

Handle gaps without inventing data

  • Invalid pixels stay nodata (NaN or a declared nodata), never 0 — zero is a valid reflectance or index value.
  • Set a minimum valid count per period and mask results below it; report the masked fraction.
  • Interpolation or gap filling is a model: label filled values, keep the original count, and never treat filled values as observations in validation.
  • Irregular sampling (orbit changes, constellation gaps, seasonal cloud) biases composites towards the clearer part of a period; report the dates that actually contributed.

Align observations in time

STAC datetime is UTC [S4]. Define periods (calendar months, 16-day windows, seasons) explicitly, and whether an observation belongs to a period by its UTC timestamp or local solar day. For bi-temporal change use comparable dates (anniversary dates or the same phenological window), the same sensor geometry (SAR: same relative orbit) and the same processing baseline where possible.

Composite of indices is not index of composite

For a nonlinear index such as NDVI, the median (or mean) of per-date indices differs in general from the index of per-band composites, and per-band medians can come from different dates. Three valid dates (red, NIR): (0.05, 0.40), (0.10, 0.20), (0.04, 0.30):

per-date NDVI          = 0.778, 0.333, 0.765     → median of NDVI = 0.765
median red, median NIR = 0.05 (date 1), 0.30 (date 3) → NDVI of medians = 0.714

Pick one, name it in metadata and outputs, and use it consistently across periods. A medoid or best-observation composite keeps real spectra from a single date (satellite preprocessing). The code below computes both, with counts:

import numpy as np
import xarray as xr

def ndvi(red, nir):
    den = nir + red
    return ((nir - red) / den).where(den != 0)

def monthly_products(red, nir, min_obs=3):
    """red, nir: decoded, masked reflectance (NaN = invalid), dims (time, y, x)."""
    valid = np.isfinite(red) & np.isfinite(nir) & ((red + nir) != 0)
    red, nir = red.where(valid), nir.where(valid)
    count = valid.resample(time="1MS").sum()
    median_of_ndvi = ndvi(red, nir).resample(time="1MS").median()
    ndvi_of_median = ndvi(red.resample(time="1MS").median(),
                          nir.resample(time="1MS").median())
    enough = count >= min_obs
    return xr.Dataset({
        "valid_count": count,
        "median_of_ndvi": median_of_ndvi.where(enough),
        "ndvi_of_median": ndvi_of_median.where(enough),
    })

Both products use the same observations: finite red/NIR pairs with a nonzero NDVI denominator. Undefined per-date NDVI never increases valid_count or satisfies min_obs. On the three dates above this returns 0.765 and 0.714 with valid_count 3. For Dask-backed arrays, test chunking along time before relying on median reductions; write outputs with explicit nodata (raster processing).

Separate seasonality and phenology from change

Vegetation indices, soil moisture, snow and water levels vary within the year. A difference between a spring and a summer image is mostly phenology. Options:

  • compare the same season or phenological stage across years;
  • use a multi-year baseline per period (e.g. same month in prior years) and express change as a departure from it;
  • use models that represent seasonality explicitly (harmonic or seasonal terms), as BFAST and CCDC do [S5, S6].

Also check for non-land-change causes: residual cloud/shadow, sensor or baseline changes, illumination and view geometry, and (SAR) moisture and incidence angle.

Thresholded differences

Bi-temporal differencing (e.g. ΔNDVI, dNBR) or SAR log ratios (log10(date2/date1) on linear power) is simple and transparent (spectral indices, SAR basics). The corresponding difference in dB is ten times that log ratio; declare the units before selecting a threshold.

  • Use the intersection of both dates' valid masks.
  • No universal threshold exists; derive and document it for the study area (reference data, histogram analysis) and report sensitivity to it.
  • A difference measures signal change, not cause; interpret with the change definition and validate.

Trend and breakpoint methods

Dense time-series methods model each pixel's trajectory and flag departures:

  • BFAST decomposes a series into trend, seasonal and remainder components and detects abrupt changes in the trend and seasonal components [S5].
  • CCDC uses all available observations, iteratively fitting harmonic functions and finding temporal breakpoints [S6, S7].
  • LandTrendr temporally segments yearly Landsat trajectories into linear segments to characterise disturbance and recovery [S8, S7].

Their parameters, input requirements and outputs are compared in references/change-methods.md. Each needs a consistent, well-masked series; gaps, undetected clouds and baseline changes create false breaks. Treat their parameters as model choices to document and test, not universal defaults.

Post-classification comparison

Comparing two independently classified maps labels every classification error in either date as change, and the accuracies of the two single-date maps say nothing about the accuracy of the change classes [S9]. Prefer mapping change directly (time-series or multi-date classification). If maps must be compared, assess the resulting change map itself.

Validate change with reference data

Change is usually rare, so assessment needs a probability sample stratified by the change map's classes (e.g. loss, gain, stable) with enough units in the change strata, reference labels that cover both dates, and area-adjusted estimates with confidence intervals [S9, S10]. Follow accuracy assessment; do not report change area as a pixel count of the change map.

Verify with Terraproof

python -m terraproof check ndvi_2023-06.tif ndvi_2024-06.tif --expect index
python -m terraproof check composite_count_2024-06.tif
python -m terraproof check source-item.json

raster.grid_alignment (dates on one grid), raster.nodata_declared, crs.defined/crs.consistent, rs.index_range (normalised indices, not dNBR or differences), rs.reflectance_range (decoded reflectance composites) and stac.provenance apply today.

For source Items, add --time-range START END to run rs.temporal_scope. For individual decoded observations, --qa QA.tif --qa-kind s2-scl runs rs.cloud_mask_applied; use landsat-qa-pixel for Landsat. Compare a decoded observation to its raw source with --reference-input RAW.tif --encoding s2-l2a (with --decode-offset, since the S2 offset depends on baseline and distributor) or landsat-c2-l2 for rs.scale_applied_once. These sampled pixel comparisons do not validate a composite against a single acquisition's QA or raw DN. --manifest run.json --require-provenance runs provenance.reproducible. For change-map reference samples and estimates, follow the current CLI example in accuracy assessment.

Not checked automatically: composite method and its consistency across periods, valid-count thresholds, phenological comparability, change thresholds, breakpoint-model parameters, and whether a detected change is real.

Sources

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.