# From time series to a map of temporal variability

How does variation through time become a spatial pattern?

One window of the Brazilian Cerrado, month by month, in near-infrared reflectance. Follow one cell's monthly cube values, their mean and how far they spread, then the same summary, computed with stars, for every cell.

Picture: Animation: 8 of the 24 months of the window's NIR08 cube values, gaps left empty, gather into a stack and merge into one map of the temporal standard deviation of 2017–2018.

References: [Pebesma and Bivand, Spatial Data Science, chapter 6, Data Cubes](https://r-spatial.org/book/06-Cubes.html), [Pebesma and Bivand, Spatial Data Science, chapter 7, Introduction to sf and stars](https://r-spatial.org/book/07-Introsf.html), [stars, st_apply](https://r-spatial.github.io/stars/reference/st_apply.html), [R statistics, sd](https://stat.ethz.ch/R-manual/R-devel/library/stats/html/sd.html) and [Pinned cube snapshot, e-sensing/cerrado_cube_reg_2018](https://huggingface.co/datasets/e-sensing/cerrado_cube_reg_2018/tree/af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85).

## 1. What was measured at this cell through time?

Here is one cell of the study window, at row 148, column 199. The image only locates it; its colours are not the values we follow.

Along a timeline, the cell keeps its place. Each month of the source has its own column on the time axis.

Now the monthly cube values of NIR08, in dimensionless surface reflectance. 13 of 24 months have a valid value. At the others the line stays open, because we do not join it across a month with no valid value.

Move across the months with the pointer, a touch or the arrow keys. A month with no valid value reads "No valid value", never zero.

Which months are missing, and what would a summary of this series leave out?

### The figure

One cell of the study window: where it lies, then its monthly cube values, in dimensionless surface reflectance. A ring under the time axis marks a month with no valid value.

Cell at row 148, column 199 (one-based) of the study window, ringed on the window's image of May 2017, which only locates the cell: its colours are not the values. Below it, the monthly cube values of NIR08, in dimensionless surface reflectance, over 2017–2018, on a time axis of every month. 13 of 24 months have a valid value. The line joins only consecutive valid months and is left open at every month with no valid value, which reads "No valid value", never zero.

| Month          | NIR08          |
| -------------- | -------------- |
| January 2017   | No valid value |
| February 2017  | No valid value |
| March 2017     | No valid value |
| April 2017     | No valid value |
| May 2017       | 0.2304         |
| June 2017      | 0.2227         |
| July 2017      | 0.2238         |
| August 2017    | 0.2309         |
| September 2017 | No valid value |
| October 2017   | No valid value |
| November 2017  | 0.3323         |
| December 2017  | No valid value |
| January 2018   | 0.2955         |
| February 2018  | No valid value |
| March 2018     | 0.2788         |
| April 2018     | No valid value |
| May 2018       | 0.2440         |
| June 2018      | 0.2275         |
| July 2018      | 0.2267         |
| August 2018    | 0.2407         |
| September 2018 | 0.1417         |
| October 2018   | 0.3011         |
| November 2018  | No valid value |
| December 2018  | No valid value |

#### How it was made

1. Cut out the study window: The pipeline finds the window in its tile of a regular cube and cuts it out of every band and month. It keeps the stored values and marks the months a pixel has none.
2. Fill the missing months: Where a pixel has no value in a month, sits fills one in on a straight line between the months around it. At either end of the series it repeats the nearest value.
3. Verify the calculation library: The pipeline checks stars against its pinned release source. sits prepares the shared inputs; stars computes these summaries.
4. Keep the measured values: The stored near-infrared values are calibrated on their native grid. The window's pictures, which only locate the cell, use the months sits fills; the stars cube keeps every missing monthly value missing, and no filled value enters a score.
5. Summarize the available months: For each period, stars keeps the spatial grid and reduces time to mean, sample standard deviation and valid-value count.
6. Follow three measured profiles: A fixed rule selects three cells with the same valid count. Their ordered observations explain the spread behind a score.
7. Prepare the pictures: The pipeline picks the brightness drawn black and white, the same for every date, so the dates can be compared. Values are packed into one byte each.
8. Write the files: The pipeline writes the files the website reads, each with a record of its sources and of how closely a rerun repeats it.

#### Reproducibility details

- A rerun on aarch64-apple-darwin23 gave the same files, byte for byte.

Every step of the pipeline:

1. Lists the band files of the regular Landsat cube on Hugging Face, at a fixed commit. `hf_cube_files("e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85")`
2. The study window, chosen as pipeline/legacy/R/01_window.R describes. `list(tile = "009010", col_off = 2208L, row_off = 1536L, size = 256L, resolution = 30L, source = "BDC", collection = "LANDSAT-OLI-16D")`
3. Identifies the exact dependency lock for stars. `"renv.lock"`
4. Reads the grid of the window's tile from a band file. `grid_read_tile(hf_resolve_url("e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", cube_files$file[cube_files$tile == study_window$tile][[1L]]))`
5. Reads the pinned stars release source methods. `variability_source_files("cache/src/stars", variability_source_lock_version(variability_lock))`
6. Verifies stars release methods against the installed package and records their source links. `variability_source_package_record(variability_release_files)`
7. Places the window in the tile's grid. `window_place(tile_grid, study_window)`
8. Crops the window out of the cube's band files by georeference, keeping crops already made. `window_crop_files("cache/windows/window_009010_c2208_r1536/reg", hf_resolve_url("e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", hf_tile_files(cube_files, study_window$tile, c("2017-01-01", "2017-02-01", "2017-03-01", "2017-04-01", "2017-05-01", "2017-06-01", "2017-07-01", "2017-08-01", "2017-09-01", "2017-10-01", "2017-11-01", "2017-12-01", "2018-01-01", "2018-02-01", "2018-03-01", "2018-04-01", "2018-05-01", "2018-06-01", "2018-07-01", "2018-08-01", "2018-09-01", "2018-10-01", "2018-11-01", "2018-12-01"))), window_grid, "e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", 8L, crop = window_crop_http_files)`
9. Names the bands and months of the crops. `window_crop_layers(window_crops, study_window$source, study_window$collection, c("2017-01-01", "2017-02-01", "2017-03-01", "2017-04-01", "2017-05-01", "2017-06-01", "2017-07-01", "2017-08-01", "2017-09-01", "2017-10-01", "2017-11-01", "2017-12-01", "2018-01-01", "2018-02-01", "2018-03-01", "2018-04-01", "2018-05-01", "2018-06-01", "2018-07-01", "2018-08-01", "2018-09-01", "2018-10-01", "2018-11-01", "2018-12-01"))`
10. Reads how sits scales the bands and marks missing values. `collection_band_values(study_window$source, study_window$collection, window_layers$bands)`
11. Reads the stored values of the crops, missing ones as NA. `window_read_raw(window_crops, window_layers$bands, window_layers$dates, band_scale, study_window$size)`
12. Calibrates the masked monthly band without filling gaps. `variability_band(window_raw, window_layers, band_scale, "NIR08")`
13. Marks the months each pixel was observed in. `window_observed_mask(window_raw)`
14. Fills the missing months along time with sits::impute_linear(), as sits does before the encoder. `window_fill_series(window_raw, band_scale)`
15. Stores every other pixel's series in one byte per value, each band between two quantiles. `window_export_series(window_series, c(0.001, 0.999 ), 2L)`
16. Picks the reflectance drawn black and white in each colour, the same for every month. `window_export_sprite_stretch(window_series, window_layers$bands, list(red = "SWIR16", green = "NIR08", blue = "RED"), c(0.02, 0.98))`
17. Draws months of the window side by side in grey, empty where no valid value, for the stack of the home picture. `site_write_png(variability_export_picture_months(variability_selected_band, list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L)))), "../web/public/data/temporal-variability-picture-months.png")`
18. Writes lossless Int16 monthly series in date chunks. `variability_export_series(variability_selected_band, "../web/public/data", list(site_dir = "../web", stars_source = "cache/src/stars", variable = "NIR08", max_bytes = 3000000L, chunk_dates = 12L, picture = list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L)))))`
19. Constructs a stars cube on native cell edges. `variability_cube(variability_selected_band, window_grid, variability_package)`
20. Derives the full period and complete calendar years. `variability_periods(variability_selected_band[["dates"]])`
21. Draws the window in every month as false-colour images in one sheet, for the website. `site_write_png(window_export_sprite(window_series, window_layers$bands, list(red = "SWIR16", green = "NIR08", blue = "RED"), series_stretch, 32L, 6L), "../web/public/data/rgb-months.png")`
22. Writes where the window is, and how its series and monthly images are stored, for the website. `site_write_json(window_export_record(study_window, window_grid, window_layers, band_scale, window_mask, series_quantised, series_stretch, cube_files, list(repo = "e-sensing/cerrado_cube_reg_2018", revision = "refs/pr/3", commit = "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85"), list(png_scale = 2L, series_quantiles = c(0.001, 0.999), series_stride = 2L, series_units = "surface reflectance", series_rgb = list(red = "SWIR16", green = "NIR08", blue = "RED"), series_stretch = c(0.02, 0.98), sprite_columns = 6L, sprite_levels = 32L, sprite_small_tile = 220L, tour_band = "NIR08"), list(series = "series.bin", mask = "series-mask.bin", sprite = "rgb-months.png", sprite_small = "rgb-months-small.png")), "../web/src/data/window.json")`
23. Uses stars to reduce time to mean, sample SD and valid count independently at every cell in each period. `lapply(variability_windows, variability_reduce, cube = variability_stars_cube)`
24. Draws the full period's temporal SD on the site's ramp, cells with no score in its checker, for the home picture. `site_write_png(variability_export_picture_map(variability_statistics, variability_windows, list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L)))), "../web/public/data/temporal-variability-picture-map.png")`
25. Writes Float64 mean/SD maps and Uint16 valid counts. `variability_export_statistics(variability_statistics, variability_windows, "../web/public/data", 3000000L)`
26. Selects median, lower and higher SD profiles with equal valid counts by a deterministic percentile rule. `variability_examples(variability_selected_band, variability_statistics[[1L]], variability_windows)`
27. Collects calibration, periods, exact teaching profiles, fixed domains and measured binary manifests. `variability_export_record(list(band = variability_selected_band, grid = window_grid, periods = variability_windows, stats = variability_statistics, examples = variability_teaching_examples, package = variability_package, source = list(repo = "e-sensing/cerrado_cube_reg_2018", revision = "refs/pr/3", commit = "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", kind = "huggingface", upstream = list(source = "BDC", collection = "LANDSAT-OLI-16D"), window = list(tile = "009010", col_off = 2208L, row_off = 1536L, size_px = 256L, resolution_m = 30L), processingRecord = "https://huggingface.co/datasets/e-sensing/cerrado_cube_reg_2018/blob/af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85/README.md"), settings = list(site_dir = "../web", stars_source = "cache/src/stars", variable = "NIR08", max_bytes = 3000000L, chunk_dates = 12L, picture = list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L))))), c(site_variability_series, site_variability_statistics, site_variability_picture_months, site_variability_picture_map))`
28. Writes the temporal variability metadata contract. `variability_export_json(variability_metadata, "../web/src/data/temporal-variability.json")`

The files:

- `temporal-variability.json`: SHA-256 `837238265334bd4dfea204fa2e91a06ba34361dafa5f891c7a3b46591762dc14`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.
- `window.json`: SHA-256 `60e731843a80b49590366d3313d69d24d23f2c23c914b42ecdff8ff53b06f9d5`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.
- `rgb-months.png`: SHA-256 `89424cbbdeeda1cd4d46f0e9b2b6c70708250c23cf3b4a2cb4064a46d8439d8e`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.

To make them again: `cd pipeline && Rscript make.R`.

## 2. How far are the values from their mean?

The calculation uses only the valid values: the 13 ringed dots. The months with no valid value stay on the time axis but take no part.

The mean of valid values, 0.2459, in dimensionless surface reflectance, is drawn as a dashed line across the months.

Take one month. In September 2018, the value lies 0.1042 below the mean, the largest departure of this cell. The solid segment is that departure, signed by its side of the mean.

The mean gives us a common reference. A departure tells us how far a valid value lies above or below it. The gaps do not enter this calculation. Each of the 13 valid months has a departure; the others have none.

Move across the months with the pointer, a touch or the arrow keys. Which months pull hardest away from the mean?

### The figure

The same cell's monthly cube values, with the mean of its valid values as a dashed line and each valid value's departure from it as a solid segment. A month with no valid value has no departure.

The monthly cube values of NIR08, in dimensionless surface reflectance, of the cell at row 148, column 199 (one-based), over 2017–2018. 13 of 24 months have a valid value; only those enter the calculation. A dashed horizontal line marks the mean of valid values, 0.2459, and a solid vertical segment joins each valid value to it: its signed departure from the mean, marked + above it and − below it. A month with no valid value contributes no departure and reads "No valid value", never zero.

| Month          | NIR08          | Departure from the mean |
| -------------- | -------------- | ----------------------- |
| January 2017   | No valid value | No valid value          |
| February 2017  | No valid value | No valid value          |
| March 2017     | No valid value | No valid value          |
| April 2017     | No valid value | No valid value          |
| May 2017       | 0.2304         | −0.0155                 |
| June 2017      | 0.2227         | −0.0232                 |
| July 2017      | 0.2238         | −0.0221                 |
| August 2017    | 0.2309         | −0.0150                 |
| September 2017 | No valid value | No valid value          |
| October 2017   | No valid value | No valid value          |
| November 2017  | 0.3323         | +0.0864                 |
| December 2017  | No valid value | No valid value          |
| January 2018   | 0.2955         | +0.0496                 |
| February 2018  | No valid value | No valid value          |
| March 2018     | 0.2788         | +0.0329                 |
| April 2018     | No valid value | No valid value          |
| May 2018       | 0.2440         | −0.0019                 |
| June 2018      | 0.2275         | −0.0184                 |
| July 2018      | 0.2267         | −0.0192                 |
| August 2018    | 0.2407         | −0.0052                 |
| September 2018 | 0.1417         | −0.1042                 |
| October 2018   | 0.3011         | +0.0552                 |
| November 2018  | No valid value | No valid value          |
| December 2018  | No valid value | No valid value          |

#### How it was made

1. Cut out the study window: The pipeline finds the window in its tile of a regular cube and cuts it out of every band and month. It keeps the stored values and marks the months a pixel has none.
2. Verify the calculation library: The pipeline checks stars against its pinned release source. sits prepares the shared inputs; stars computes these summaries.
3. Keep the measured values: The stored near-infrared values are calibrated on their native grid. The window's pictures, which only locate the cell, use the months sits fills; the stars cube keeps every missing monthly value missing, and no filled value enters a score.
4. Summarize the available months: For each period, stars keeps the spatial grid and reduces time to mean, sample standard deviation and valid-value count.
5. Follow three measured profiles: A fixed rule selects three cells with the same valid count. Their ordered observations explain the spread behind a score.
6. Write the files: The pipeline writes the files the website reads, each with a record of its sources and of how closely a rerun repeats it.

#### Reproducibility details

- A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.

Every step of the pipeline:

1. Lists the band files of the regular Landsat cube on Hugging Face, at a fixed commit. `hf_cube_files("e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85")`
2. The study window, chosen as pipeline/legacy/R/01_window.R describes. `list(tile = "009010", col_off = 2208L, row_off = 1536L, size = 256L, resolution = 30L, source = "BDC", collection = "LANDSAT-OLI-16D")`
3. Identifies the exact dependency lock for stars. `"renv.lock"`
4. Reads the grid of the window's tile from a band file. `grid_read_tile(hf_resolve_url("e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", cube_files$file[cube_files$tile == study_window$tile][[1L]]))`
5. Reads the pinned stars release source methods. `variability_source_files("cache/src/stars", variability_source_lock_version(variability_lock))`
6. Verifies stars release methods against the installed package and records their source links. `variability_source_package_record(variability_release_files)`
7. Places the window in the tile's grid. `window_place(tile_grid, study_window)`
8. Crops the window out of the cube's band files by georeference, keeping crops already made. `window_crop_files("cache/windows/window_009010_c2208_r1536/reg", hf_resolve_url("e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", hf_tile_files(cube_files, study_window$tile, c("2017-01-01", "2017-02-01", "2017-03-01", "2017-04-01", "2017-05-01", "2017-06-01", "2017-07-01", "2017-08-01", "2017-09-01", "2017-10-01", "2017-11-01", "2017-12-01", "2018-01-01", "2018-02-01", "2018-03-01", "2018-04-01", "2018-05-01", "2018-06-01", "2018-07-01", "2018-08-01", "2018-09-01", "2018-10-01", "2018-11-01", "2018-12-01"))), window_grid, "e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", 8L, crop = window_crop_http_files)`
9. Names the bands and months of the crops. `window_crop_layers(window_crops, study_window$source, study_window$collection, c("2017-01-01", "2017-02-01", "2017-03-01", "2017-04-01", "2017-05-01", "2017-06-01", "2017-07-01", "2017-08-01", "2017-09-01", "2017-10-01", "2017-11-01", "2017-12-01", "2018-01-01", "2018-02-01", "2018-03-01", "2018-04-01", "2018-05-01", "2018-06-01", "2018-07-01", "2018-08-01", "2018-09-01", "2018-10-01", "2018-11-01", "2018-12-01"))`
10. Reads how sits scales the bands and marks missing values. `collection_band_values(study_window$source, study_window$collection, window_layers$bands)`
11. Reads the stored values of the crops, missing ones as NA. `window_read_raw(window_crops, window_layers$bands, window_layers$dates, band_scale, study_window$size)`
12. Calibrates the masked monthly band without filling gaps. `variability_band(window_raw, window_layers, band_scale, "NIR08")`
13. Draws months of the window side by side in grey, empty where no valid value, for the stack of the home picture. `site_write_png(variability_export_picture_months(variability_selected_band, list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L)))), "../web/public/data/temporal-variability-picture-months.png")`
14. Writes lossless Int16 monthly series in date chunks. `variability_export_series(variability_selected_band, "../web/public/data", list(site_dir = "../web", stars_source = "cache/src/stars", variable = "NIR08", max_bytes = 3000000L, chunk_dates = 12L, picture = list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L)))))`
15. Constructs a stars cube on native cell edges. `variability_cube(variability_selected_band, window_grid, variability_package)`
16. Derives the full period and complete calendar years. `variability_periods(variability_selected_band[["dates"]])`
17. Uses stars to reduce time to mean, sample SD and valid count independently at every cell in each period. `lapply(variability_windows, variability_reduce, cube = variability_stars_cube)`
18. Draws the full period's temporal SD on the site's ramp, cells with no score in its checker, for the home picture. `site_write_png(variability_export_picture_map(variability_statistics, variability_windows, list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L)))), "../web/public/data/temporal-variability-picture-map.png")`
19. Writes Float64 mean/SD maps and Uint16 valid counts. `variability_export_statistics(variability_statistics, variability_windows, "../web/public/data", 3000000L)`
20. Selects median, lower and higher SD profiles with equal valid counts by a deterministic percentile rule. `variability_examples(variability_selected_band, variability_statistics[[1L]], variability_windows)`
21. Collects calibration, periods, exact teaching profiles, fixed domains and measured binary manifests. `variability_export_record(list(band = variability_selected_band, grid = window_grid, periods = variability_windows, stats = variability_statistics, examples = variability_teaching_examples, package = variability_package, source = list(repo = "e-sensing/cerrado_cube_reg_2018", revision = "refs/pr/3", commit = "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", kind = "huggingface", upstream = list(source = "BDC", collection = "LANDSAT-OLI-16D"), window = list(tile = "009010", col_off = 2208L, row_off = 1536L, size_px = 256L, resolution_m = 30L), processingRecord = "https://huggingface.co/datasets/e-sensing/cerrado_cube_reg_2018/blob/af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85/README.md"), settings = list(site_dir = "../web", stars_source = "cache/src/stars", variable = "NIR08", max_bytes = 3000000L, chunk_dates = 12L, picture = list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L))))), c(site_variability_series, site_variability_statistics, site_variability_picture_months, site_variability_picture_map))`
22. Writes the temporal variability metadata contract. `variability_export_json(variability_metadata, "../web/src/data/temporal-variability.json")`

The files:

- `temporal-variability.json`: SHA-256 `837238265334bd4dfea204fa2e91a06ba34361dafa5f891c7a3b46591762dc14`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.

To make them again: `cd pipeline && Rscript make.R`.

## 3. How can we summarize those departures?

Could we simply add the departures? Those above the mean and those below it cancel: added up, they come to zero, however far each value strays.

Once squared, no departure can be negative, so none can cancel another, and a large departure weighs much more than a small one. The bars under the profile are the squared departures, on their own axis. Together they add up to 0.027142.

Divide that sum by the number of valid values minus one, 12: 0.027142 ÷ 12 = 0.002262. This is the variance, in squared units.

Its square root, 0.0476, is the temporal standard deviation, back in dimensionless surface reflectance. It rests on the 13 valid values, not on all 24 months.

Standard deviation turns the spread of the valid values into one number. Larger departures tend to produce a larger score, but the score cannot tell us when those departures occurred.

Here are two other cells, each with 13 valid values, on the same axis. Which spreads more? Choose one, or show the answer, and compare their scores.

### The figure

The same cell's departures from the mean, then their squares as bars on a separate axis under the profile, each joined to its value, combined step by step into one score. Last, two other cells on the same axis, to compare.

The departures from the mean of the monthly cube values of NIR08, in dimensionless surface reflectance, of the cell at row 148, column 199 (one-based), over 2017–2018. 13 of 24 months have a valid value. Each departure is squared and drawn as a bar on its own axis under its month; a month with no valid value reads "No valid value" and has neither.

| Month          | Departure from the mean | Squared departure |
| -------------- | ----------------------- | ----------------- |
| January 2017   | No valid value          | No valid value    |
| February 2017  | No valid value          | No valid value    |
| March 2017     | No valid value          | No valid value    |
| April 2017     | No valid value          | No valid value    |
| May 2017       | −0.0155                 | 0.000239          |
| June 2017      | −0.0232                 | 0.000536          |
| July 2017      | −0.0221                 | 0.000486          |
| August 2017    | −0.0150                 | 0.000224          |
| September 2017 | No valid value          | No valid value    |
| October 2017   | No valid value          | No valid value    |
| November 2017  | +0.0864                 | 0.007473          |
| December 2017  | No valid value          | No valid value    |
| January 2018   | +0.0496                 | 0.002465          |
| February 2018  | No valid value          | No valid value    |
| March 2018     | +0.0329                 | 0.001085          |
| April 2018     | No valid value          | No valid value    |
| May 2018       | −0.0019                 | 0.000003          |
| June 2018      | −0.0184                 | 0.000337          |
| July 2018      | −0.0192                 | 0.000367          |
| August 2018    | −0.0052                 | 0.000027          |
| September 2018 | −0.1042                 | 0.010848          |
| October 2018   | +0.0552                 | 0.003052          |
| November 2018  | No valid value          | No valid value    |
| December 2018  | No valid value          | No valid value    |

Added up, the 13 departures give 0.0000: those above the mean cancel those below. Squared, no departure can be negative: the 13 squares add up to 0.027142. 0.027142 ÷ 12 = 0.002262, the variance: the sum of squares divided by n − 1 = 12. √0.002262 = 0.0476, Temporal standard deviation, in dimensionless surface reflectance, from 13 valid values. This is the sample standard deviation.

Last, the figure shows two other cells on the same axis, with the same number of valid values, and asks which spreads more before it reveals their scores:

| Cell                       | Valid values | Temporal standard deviation |
| -------------------------- | ------------ | --------------------------- |
| Cell at row 40, column 3   | 13           | 0.0291                      |
| Cell at row 136, column 74 | 13           | 0.0849                      |

#### The equation of the temporal standard deviation

The score of the main beats, written out in full:

`s = √( Σ (xᵢ − x̄)² ÷ (n − 1) )`

xᵢ is each valid monthly value, x̄ their mean, and n how many valid values there are. A month with no valid value does not enter the sum, nor n.

For this cell, n = 13: √(0.027142 ÷ 12) = √0.002262 = 0.0476, in dimensionless surface reflectance.

The pipeline computed it with stats::sd(), stats 4.6.0, whose denominator is n − 1: the sample standard deviation. Dividing by n instead gives the population standard deviation, a little smaller for the same values. At least 2 valid values are needed, so a cell with fewer has no score. Reference: [R statistics, sd](https://stat.ethz.ch/R-manual/R-devel/library/stats/html/sd.html).

#### How it was made

1. Cut out the study window: The pipeline finds the window in its tile of a regular cube and cuts it out of every band and month. It keeps the stored values and marks the months a pixel has none.
2. Verify the calculation library: The pipeline checks stars against its pinned release source. sits prepares the shared inputs; stars computes these summaries.
3. Keep the measured values: The stored near-infrared values are calibrated on their native grid. The window's pictures, which only locate the cell, use the months sits fills; the stars cube keeps every missing monthly value missing, and no filled value enters a score.
4. Summarize the available months: For each period, stars keeps the spatial grid and reduces time to mean, sample standard deviation and valid-value count.
5. Follow three measured profiles: A fixed rule selects three cells with the same valid count. Their ordered observations explain the spread behind a score.
6. Write the files: The pipeline writes the files the website reads, each with a record of its sources and of how closely a rerun repeats it.

#### Reproducibility details

- A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.

Every step of the pipeline:

1. Lists the band files of the regular Landsat cube on Hugging Face, at a fixed commit. `hf_cube_files("e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85")`
2. The study window, chosen as pipeline/legacy/R/01_window.R describes. `list(tile = "009010", col_off = 2208L, row_off = 1536L, size = 256L, resolution = 30L, source = "BDC", collection = "LANDSAT-OLI-16D")`
3. Identifies the exact dependency lock for stars. `"renv.lock"`
4. Reads the grid of the window's tile from a band file. `grid_read_tile(hf_resolve_url("e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", cube_files$file[cube_files$tile == study_window$tile][[1L]]))`
5. Reads the pinned stars release source methods. `variability_source_files("cache/src/stars", variability_source_lock_version(variability_lock))`
6. Verifies stars release methods against the installed package and records their source links. `variability_source_package_record(variability_release_files)`
7. Places the window in the tile's grid. `window_place(tile_grid, study_window)`
8. Crops the window out of the cube's band files by georeference, keeping crops already made. `window_crop_files("cache/windows/window_009010_c2208_r1536/reg", hf_resolve_url("e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", hf_tile_files(cube_files, study_window$tile, c("2017-01-01", "2017-02-01", "2017-03-01", "2017-04-01", "2017-05-01", "2017-06-01", "2017-07-01", "2017-08-01", "2017-09-01", "2017-10-01", "2017-11-01", "2017-12-01", "2018-01-01", "2018-02-01", "2018-03-01", "2018-04-01", "2018-05-01", "2018-06-01", "2018-07-01", "2018-08-01", "2018-09-01", "2018-10-01", "2018-11-01", "2018-12-01"))), window_grid, "e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", 8L, crop = window_crop_http_files)`
9. Names the bands and months of the crops. `window_crop_layers(window_crops, study_window$source, study_window$collection, c("2017-01-01", "2017-02-01", "2017-03-01", "2017-04-01", "2017-05-01", "2017-06-01", "2017-07-01", "2017-08-01", "2017-09-01", "2017-10-01", "2017-11-01", "2017-12-01", "2018-01-01", "2018-02-01", "2018-03-01", "2018-04-01", "2018-05-01", "2018-06-01", "2018-07-01", "2018-08-01", "2018-09-01", "2018-10-01", "2018-11-01", "2018-12-01"))`
10. Reads how sits scales the bands and marks missing values. `collection_band_values(study_window$source, study_window$collection, window_layers$bands)`
11. Reads the stored values of the crops, missing ones as NA. `window_read_raw(window_crops, window_layers$bands, window_layers$dates, band_scale, study_window$size)`
12. Calibrates the masked monthly band without filling gaps. `variability_band(window_raw, window_layers, band_scale, "NIR08")`
13. Draws months of the window side by side in grey, empty where no valid value, for the stack of the home picture. `site_write_png(variability_export_picture_months(variability_selected_band, list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L)))), "../web/public/data/temporal-variability-picture-months.png")`
14. Writes lossless Int16 monthly series in date chunks. `variability_export_series(variability_selected_band, "../web/public/data", list(site_dir = "../web", stars_source = "cache/src/stars", variable = "NIR08", max_bytes = 3000000L, chunk_dates = 12L, picture = list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L)))))`
15. Constructs a stars cube on native cell edges. `variability_cube(variability_selected_band, window_grid, variability_package)`
16. Derives the full period and complete calendar years. `variability_periods(variability_selected_band[["dates"]])`
17. Uses stars to reduce time to mean, sample SD and valid count independently at every cell in each period. `lapply(variability_windows, variability_reduce, cube = variability_stars_cube)`
18. Draws the full period's temporal SD on the site's ramp, cells with no score in its checker, for the home picture. `site_write_png(variability_export_picture_map(variability_statistics, variability_windows, list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L)))), "../web/public/data/temporal-variability-picture-map.png")`
19. Writes Float64 mean/SD maps and Uint16 valid counts. `variability_export_statistics(variability_statistics, variability_windows, "../web/public/data", 3000000L)`
20. Selects median, lower and higher SD profiles with equal valid counts by a deterministic percentile rule. `variability_examples(variability_selected_band, variability_statistics[[1L]], variability_windows)`
21. Collects calibration, periods, exact teaching profiles, fixed domains and measured binary manifests. `variability_export_record(list(band = variability_selected_band, grid = window_grid, periods = variability_windows, stats = variability_statistics, examples = variability_teaching_examples, package = variability_package, source = list(repo = "e-sensing/cerrado_cube_reg_2018", revision = "refs/pr/3", commit = "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", kind = "huggingface", upstream = list(source = "BDC", collection = "LANDSAT-OLI-16D"), window = list(tile = "009010", col_off = 2208L, row_off = 1536L, size_px = 256L, resolution_m = 30L), processingRecord = "https://huggingface.co/datasets/e-sensing/cerrado_cube_reg_2018/blob/af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85/README.md"), settings = list(site_dir = "../web", stars_source = "cache/src/stars", variable = "NIR08", max_bytes = 3000000L, chunk_dates = 12L, picture = list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L))))), c(site_variability_series, site_variability_statistics, site_variability_picture_months, site_variability_picture_map))`
22. Writes the temporal variability metadata contract. `variability_export_json(variability_metadata, "../web/src/data/temporal-variability.json")`

The files:

- `temporal-variability.json`: SHA-256 `837238265334bd4dfea204fa2e91a06ba34361dafa5f891c7a3b46591762dc14`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.

To make them again: `cd pipeline && Rscript make.R`.

Try it: [Two cells, which spreads more?](/interactive-images/temporal-variability-profile.md)

## 4. What happens when we repeat this for every cell?

The cell we followed has a temporal standard deviation of 0.0476. Here it stands at its own place on the grid of the study window, at row 148, column 199, with its profile beside it.

Repeating the calculation retains the spatial grid and reduces time to one score per cell. stars applied the same reduction to every cell, each on its own monthly values, and this map is the result. The colours run from 0 to 0.1849, and that scale stays the same whatever the period.

Select a place to recover its contributing observations: click or tap the map, move across it with the arrow keys, or pick one of the example cells. The profile, its mean, its score and its count change together, and the table of observations lists every month.

A score is only as solid as the values behind it. Switch the map to its valid months: each cell's count of valid monthly values, from 0 to 15. A cell with fewer than 2 valid values has no score at all, and the map marks it apart from a score of zero.

Now inspect how the chosen period and its available values affect the summary. The periods are 2017–2018, 2017 and 2018; the cell stays selected when the period changes, and each period's button shows how many of its months have a valid value there.

Explore the map. Where is the spread largest, and on how many values does it rest? A difference between years is not land-cover change: each year has its own valid months, and the score describes how far the values spread, not why.

### The figure

The temporal standard deviation of every cell of the study window, on one colour scale from 0 for every period, beside the selected cell's monthly cube values. A checker marks a cell with too few valid values for a score; a second view maps each cell's count of valid monthly values.

A map of the temporal standard deviation of the monthly cube values of NIR08, in dimensionless surface reflectance, for each of the 256 × 256 cells of the study window. Computed with stars in R, the same summary for every cell. Its colours run on the cividis ramp from 0 to 0.1849, the same scale for every period. A cell with fewer than 2 valid values has no score and is drawn in a pink checker, apart from a score of 0. A second view maps each cell's count of valid monthly values ("Valid monthly values"), from 0 to 15, in purples that lighten with the count, with 0 in orange. Beside the map, the selected cell's profile on a time axis of every month, with the months outside the chosen period faded and the mean of its valid values as a dashed line. The periods are 2017–2018, 2017 and 2018; changing the period keeps the cell.

Cell at row 148, column 199 (one-based), the cell followed in the earlier steps:

| Period    | Valid values | Mean of valid values | Temporal standard deviation |
| --------- | ------------ | -------------------- | --------------------------- |
| 2017–2018 | 13 of 24     | 0.2459               | 0.0476                      |
| 2017      | 5 of 12      | 0.2480               | 0.0473                      |
| 2018      | 8 of 12      | 0.2445               | 0.0509                      |

#### How it was made

1. Cut out the study window: The pipeline finds the window in its tile of a regular cube and cuts it out of every band and month. It keeps the stored values and marks the months a pixel has none.
2. Fill the missing months: Where a pixel has no value in a month, sits fills one in on a straight line between the months around it. At either end of the series it repeats the nearest value.
3. Verify the calculation library: The pipeline checks stars against its pinned release source. sits prepares the shared inputs; stars computes these summaries.
4. Keep the measured values: The stored near-infrared values are calibrated on their native grid. The window's pictures, which only locate the cell, use the months sits fills; the stars cube keeps every missing monthly value missing, and no filled value enters a score.
5. Summarize the available months: For each period, stars keeps the spatial grid and reduces time to mean, sample standard deviation and valid-value count.
6. Follow three measured profiles: A fixed rule selects three cells with the same valid count. Their ordered observations explain the spread behind a score.
7. Prepare the pictures: The pipeline picks the brightness drawn black and white, the same for every date, so the dates can be compared. Values are packed into one byte each.
8. Write the files: The pipeline writes the files the website reads, each with a record of its sources and of how closely a rerun repeats it.

#### Reproducibility details

- A rerun on aarch64-apple-darwin23 gave the same files, byte for byte.

Every step of the pipeline:

1. Lists the band files of the regular Landsat cube on Hugging Face, at a fixed commit. `hf_cube_files("e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85")`
2. The study window, chosen as pipeline/legacy/R/01_window.R describes. `list(tile = "009010", col_off = 2208L, row_off = 1536L, size = 256L, resolution = 30L, source = "BDC", collection = "LANDSAT-OLI-16D")`
3. Identifies the exact dependency lock for stars. `"renv.lock"`
4. Reads the grid of the window's tile from a band file. `grid_read_tile(hf_resolve_url("e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", cube_files$file[cube_files$tile == study_window$tile][[1L]]))`
5. Reads the pinned stars release source methods. `variability_source_files("cache/src/stars", variability_source_lock_version(variability_lock))`
6. Verifies stars release methods against the installed package and records their source links. `variability_source_package_record(variability_release_files)`
7. Places the window in the tile's grid. `window_place(tile_grid, study_window)`
8. Crops the window out of the cube's band files by georeference, keeping crops already made. `window_crop_files("cache/windows/window_009010_c2208_r1536/reg", hf_resolve_url("e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", hf_tile_files(cube_files, study_window$tile, c("2017-01-01", "2017-02-01", "2017-03-01", "2017-04-01", "2017-05-01", "2017-06-01", "2017-07-01", "2017-08-01", "2017-09-01", "2017-10-01", "2017-11-01", "2017-12-01", "2018-01-01", "2018-02-01", "2018-03-01", "2018-04-01", "2018-05-01", "2018-06-01", "2018-07-01", "2018-08-01", "2018-09-01", "2018-10-01", "2018-11-01", "2018-12-01"))), window_grid, "e-sensing/cerrado_cube_reg_2018", "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", 8L, crop = window_crop_http_files)`
9. Names the bands and months of the crops. `window_crop_layers(window_crops, study_window$source, study_window$collection, c("2017-01-01", "2017-02-01", "2017-03-01", "2017-04-01", "2017-05-01", "2017-06-01", "2017-07-01", "2017-08-01", "2017-09-01", "2017-10-01", "2017-11-01", "2017-12-01", "2018-01-01", "2018-02-01", "2018-03-01", "2018-04-01", "2018-05-01", "2018-06-01", "2018-07-01", "2018-08-01", "2018-09-01", "2018-10-01", "2018-11-01", "2018-12-01"))`
10. Reads how sits scales the bands and marks missing values. `collection_band_values(study_window$source, study_window$collection, window_layers$bands)`
11. Reads the stored values of the crops, missing ones as NA. `window_read_raw(window_crops, window_layers$bands, window_layers$dates, band_scale, study_window$size)`
12. Calibrates the masked monthly band without filling gaps. `variability_band(window_raw, window_layers, band_scale, "NIR08")`
13. Marks the months each pixel was observed in. `window_observed_mask(window_raw)`
14. Fills the missing months along time with sits::impute_linear(), as sits does before the encoder. `window_fill_series(window_raw, band_scale)`
15. Stores every other pixel's series in one byte per value, each band between two quantiles. `window_export_series(window_series, c(0.001, 0.999 ), 2L)`
16. Picks the reflectance drawn black and white in each colour, the same for every month. `window_export_sprite_stretch(window_series, window_layers$bands, list(red = "SWIR16", green = "NIR08", blue = "RED"), c(0.02, 0.98))`
17. Draws months of the window side by side in grey, empty where no valid value, for the stack of the home picture. `site_write_png(variability_export_picture_months(variability_selected_band, list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L)))), "../web/public/data/temporal-variability-picture-months.png")`
18. Writes lossless Int16 monthly series in date chunks. `variability_export_series(variability_selected_band, "../web/public/data", list(site_dir = "../web", stars_source = "cache/src/stars", variable = "NIR08", max_bytes = 3000000L, chunk_dates = 12L, picture = list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L)))))`
19. Constructs a stars cube on native cell edges. `variability_cube(variability_selected_band, window_grid, variability_package)`
20. Derives the full period and complete calendar years. `variability_periods(variability_selected_band[["dates"]])`
21. Writes where the window is, and how its series and monthly images are stored, for the website. `site_write_json(window_export_record(study_window, window_grid, window_layers, band_scale, window_mask, series_quantised, series_stretch, cube_files, list(repo = "e-sensing/cerrado_cube_reg_2018", revision = "refs/pr/3", commit = "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85"), list(png_scale = 2L, series_quantiles = c(0.001, 0.999), series_stride = 2L, series_units = "surface reflectance", series_rgb = list(red = "SWIR16", green = "NIR08", blue = "RED"), series_stretch = c(0.02, 0.98), sprite_columns = 6L, sprite_levels = 32L, sprite_small_tile = 220L, tour_band = "NIR08"), list(series = "series.bin", mask = "series-mask.bin", sprite = "rgb-months.png", sprite_small = "rgb-months-small.png")), "../web/src/data/window.json")`
22. Uses stars to reduce time to mean, sample SD and valid count independently at every cell in each period. `lapply(variability_windows, variability_reduce, cube = variability_stars_cube)`
23. Draws the full period's temporal SD on the site's ramp, cells with no score in its checker, for the home picture. `site_write_png(variability_export_picture_map(variability_statistics, variability_windows, list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L)))), "../web/public/data/temporal-variability-picture-map.png")`
24. Writes Float64 mean/SD maps and Uint16 valid counts. `variability_export_statistics(variability_statistics, variability_windows, "../web/public/data", 3000000L)`
25. Selects median, lower and higher SD profiles with equal valid counts by a deterministic percentile rule. `variability_examples(variability_selected_band, variability_statistics[[1L]], variability_windows)`
26. Collects calibration, periods, exact teaching profiles, fixed domains and measured binary manifests. `variability_export_record(list(band = variability_selected_band, grid = window_grid, periods = variability_windows, stats = variability_statistics, examples = variability_teaching_examples, package = variability_package, source = list(repo = "e-sensing/cerrado_cube_reg_2018", revision = "refs/pr/3", commit = "af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85", kind = "huggingface", upstream = list(source = "BDC", collection = "LANDSAT-OLI-16D"), window = list(tile = "009010", col_off = 2208L, row_off = 1536L, size_px = 256L, resolution_m = 30L), processingRecord = "https://huggingface.co/datasets/e-sensing/cerrado_cube_reg_2018/blob/af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85/README.md"), settings = list(site_dir = "../web", stars_source = "cache/src/stars", variable = "NIR08", max_bytes = 3000000L, chunk_dates = 12L, picture = list(count = 8L, min_valid = 0.5, size = 128L, period = "full", ramp = c("#00224e", "#3b496c", "#7c7b78", "#bcaf6f", "#fee838"), checker = list(c(214L, 51L, 108L), c(253L, 224L, 239L))))), c(site_variability_series, site_variability_statistics, site_variability_picture_months, site_variability_picture_map))`
27. Writes the temporal variability metadata contract. `variability_export_json(variability_metadata, "../web/src/data/temporal-variability.json")`

The files:

- `temporal-variability.json`: SHA-256 `837238265334bd4dfea204fa2e91a06ba34361dafa5f891c7a3b46591762dc14`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.
- `window.json`: SHA-256 `60e731843a80b49590366d3313d69d24d23f2c23c914b42ecdff8ff53b06f9d5`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.
- `temporal-variability-stats-full.bin`: SHA-256 `fc7690eb504c9a568bf4e8b1f2d7b9ac4e8e62db1ebcc0fb20030aa685288259`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.
- `temporal-variability-count-full.bin`: SHA-256 `27e546f9b47d6d5cb65bbb66879da607c51ea981669155b9727fb91ad6fe68d5`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.
- `temporal-variability-stats-2017.bin`: SHA-256 `fbb0b2585119143f60acc94231d11617e5b8b167453fe88999ad8fece637afe9`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.
- `temporal-variability-count-2017.bin`: SHA-256 `205e75d1eaff1b08fe4c66349879f771d91f994c2ba76c9f78a6a1c4bdecce58`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.
- `temporal-variability-stats-2018.bin`: SHA-256 `2bf202b5f1ba3a3011a0b7fc4a4d8544698f82e63899227220c3396f4b4e58e9`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.
- `temporal-variability-count-2018.bin`: SHA-256 `b1c47a1f08cea9827d441edda46e513d8dadab056b11922acace8df6475fc96b`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.
- `temporal-variability-series-000.bin`: SHA-256 `52268606dbacf18e5c915b92897e69422776ee0288f0a0dde3f444a75b9f157d`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.
- `temporal-variability-series-001.bin`: SHA-256 `30c069d9c8b1f066a5cc5f5dd050ada90e7de99dd8d1b5a5cf2bdc700a18d32e`. A rerun on aarch64-apple-darwin23 gave the same file, byte for byte.

To make them again: `cd pipeline && Rscript make.R`.

Try it: [A map of temporal variability](/interactive-images/temporal-variability-map.md)

## Next

References: [Pebesma and Bivand, Spatial Data Science, chapter 6, Data Cubes](https://r-spatial.org/book/06-Cubes.html), [Pebesma and Bivand, Spatial Data Science, chapter 7, Introduction to sf and stars](https://r-spatial.org/book/07-Introsf.html), [stars, st_apply](https://r-spatial.github.io/stars/reference/st_apply.html), [R statistics, sd](https://stat.ethz.ch/R-manual/R-devel/library/stats/html/sd.html) and [Pinned cube snapshot, e-sensing/cerrado_cube_reg_2018](https://huggingface.co/datasets/e-sensing/cerrado_cube_reg_2018/tree/af1d4f67fee0fcaba35941f4ac5d7d9e87d4fa85).
