Skip to content

Paddock time series

Three closely-related functions that turn per-pixel rasters into per-paddock summaries:

Smoothed per-paddock NDVI medians on (paddock, time)

Smoothed per-paddock NDVI medians on (paddock, time)
Function What it produces
make_paddock_time_series Per-paddock medians for every band + index at every timestep, on dims (paddock, time).
make_yearly_paddock_time_series The same dataset split into one slice per calendar year, with a doy (day-of-year) coordinate attached.
make_smoothed_paddock_time_series The same data resampled to a fixed cadence, gap-filled with PCHIP interpolation, and smoothed with a Savitzky-Golay filter.

All three persist Zarr v2 outputs under troi.tmp_dir, named from the paddocks file's stem:

  • {stem}_timeseries.zarr
  • {stem}_timeseries_{year}.zarr (one per year)
  • {stem}_timeseries_smoothed.zarr

These functions are the pivot from pixel-space to paddock-space — the central time-series dataset that phenology and plotting consume.


Example: produce a paddock × time table

from datetime import date
from troi.troi import Troi
from PaddockTS.Phenology.make_paddock_time_series import make_paddock_time_series

q = Troi(
    bbox=[148.36265, -33.52606, 148.38265, -33.50606],
    start=date(2024, 1, 1),
    end=date(2024, 12, 31),
    stub="ts_demo",
)

ts = make_paddock_time_series(q)
print(ts)
# <xarray.Dataset>
# Dimensions:      (paddock: 12, time: 73)
# Coordinates:
#   * paddock      (paddock) <U3 '1' '2' '3' ... '12'
#   * time         (time) datetime64[ns] 2024-01-03 ... 2024-12-29
#     spatial_ref  int32 ...
# Data variables:
#     nbart_blue   (paddock, time) float64 ...
#     nbart_green  (paddock, time) float64 ...
#     ...
#     NDVI         (paddock, time) float64 ...
#     CFI          (paddock, time) float64 ...
#     NIRv         (paddock, time) float64 ...
#     NDTI         (paddock, time) float64 ...
#     CAI          (paddock, time) float64 ...

# NDVI of paddock 1 over the year, as a pandas Series
ndvi_p1 = ts.NDVI.sel(paddock="1").to_pandas()
ndvi_p1.plot()

The paddock coordinate is always strings — '1', '2', … — so ts.sel(paddock="1") works whether your IDs are numeric or human-readable labels.


Example: split by year + plot DOY-aligned NDVI

make_yearly_paddock_time_series adds a doy (1–366) coordinate so multi-year series can be overlaid on a common DOY axis:

import matplotlib.pyplot as plt
from PaddockTS.Phenology.make_yearly_paddock_time_series import make_yearly_paddock_time_series

# Use a multi-year troi
q = Troi(
    bbox=[148.36265, -33.52606, 148.38265, -33.50606],
    start=date(2022, 1, 1),
    end=date(2024, 12, 31),
    stub="yearly_demo",
)
yearly = make_yearly_paddock_time_series(q)
# {2022: <Dataset>, 2023: <Dataset>, 2024: <Dataset>}

fig, ax = plt.subplots()
for year, ds in yearly.items():
    ax.plot(ds.doy, ds.NDVI.sel(paddock="1"), label=str(year))
ax.set_xlabel("DOY")
ax.set_ylabel("NDVI")
ax.legend()

Example: smoothed series for phenology

Sentinel-2 revisit gaps and cloud-mask drops leave irregular series. make_smoothed_paddock_time_series produces a uniform, smoothed version suitable for phenology fitting and plotting:

from PaddockTS.Phenology.make_smoothed_paddock_time_series import make_smoothed_paddock_time_series

smoothed = make_smoothed_paddock_time_series(
    q,
    days=10,          # 10-day median resample
    window_length=7,  # Savitzky-Golay window (odd; coerced if even)
    polyorder=2,      # SG polynomial order (< window_length)
)

# Compare raw vs. smoothed for one paddock
raw = ts.NDVI.sel(paddock="1")
smo = smoothed.NDVI.sel(paddock="1")
ax = raw.plot.scatter(x="time", label="raw")
smo.plot(ax=ax.axes, color="red", label="smoothed")
ax.axes.legend()

Bring your own paddocks

All three functions accept paddocks_filepath. If None, they default to the SAM paddocks at troi.sam_paddocks_path (running SAM if it hasn't been run yet). Any GeoPackage / Shapefile / GeoJSON with a paddock column works; load_user_paddocks will derive missing columns.

ts = make_paddock_time_series(
    q,
    paddocks_filepath="/path/to/my_paddocks.gpkg",
)

The output Zarr is named from the file stem (my_paddocks_timeseries.zarr), so SAM and user runs coexist in the same tmp_dir without overwriting each other.


Reference

make_paddock_time_series

PaddockTS.Phenology.make_paddock_time_series.make_paddock_time_series

make_paddock_time_series(troi: Troi, ds_sentinel2=None, paddocks_filepath=None, crs='epsg:6933')

Compute per-paddock medians for every band at every timestep.

Steps:

  1. Compute the five spectral indices (NDVI, CFI, NIRv, NDTI, CAI) and add them to the input dataset.
  2. Rasterise paddock polygons to integer IDs aligned with the Sentinel-2 grid.
  3. For each band, in parallel across processes, compute the per-paddock NaN-aware median across pixels at every timestep.
  4. Stitch results back into an xarray Dataset on dims (paddock, time) and persist as Zarr v2 to {paddocks_filepath stem}_timeseries.zarr.

Parameters:

Name Type Description Default
troi Troi

The :class:troi.Troi.

required
ds_sentinel2

Optional in-memory Sentinel-2 dataset (with the five indices already added, or they will be computed). If None, the cloud-masked window with indices comes from the pysentinel2 cube.

None
paddocks_filepath

Path to a GeoPackage (.gpkg) containing paddock polygons (must include a paddock column for IDs). If None, defaults to Paths(troi).sam_paddocks (loaded or generated via :func:PaddockTS.PaddockSegmentation.get_paddocks).

None
crs

Equal-area CRS to write onto the dataset for georeferencing the rasterised mask. Defaults to EPSG:6933 (WGS84 / NSIDC EASE-Grid 2.0 Global).

'epsg:6933'

Returns:

Type Description

xarray.Dataset: Per-paddock medians on dims (paddock, time)

with one data variable per Sentinel-2 band and per spectral

index. Also persisted to {paddocks_filepath stem}_timeseries.zarr.

make_yearly_paddock_time_series

PaddockTS.Phenology.make_yearly_paddock_time_series.make_yearly_paddock_time_series

make_yearly_paddock_time_series(troi, ds_paddockTS=None, paddocks_filepath=None)

Persist one Zarr per year and return the same data as a dict.

Loads the paddockTS Zarr if not provided, calls :func:split_paddock_time_series_by_year, and writes each per-year slice as Zarr v2 to {paddocks_filepath stem}_timeseries_{year}.zarr.

Parameters:

Name Type Description Default
troi

The :class:troi.Troi.

required
ds_paddockTS

Optional in-memory paddockTS dataset (typically the smoothed series). If None, opens (or generates, then opens) the cached smoothed timeseries zarr.

None
paddocks_filepath

Path to the paddocks GeoPackage. Used to derive the timeseries zarr path. If None, defaults to Paths(troi).sam_paddocks.

None

Returns:

Type Description

dict[int, xarray.Dataset]: Mapping {year: ds_year}. Each

per-year slice is also persisted to disk.

make_smoothed_paddock_time_series

PaddockTS.Phenology.make_smoothed_paddock_time_series.make_smoothed_paddock_time_series

make_smoothed_paddock_time_series(troi, ds_paddockTS=None, paddocks_filepath=None, days=10, window_length=7, polyorder=2)

Resample-then-interpolate-then-smooth all time-dependent variables.

Pipeline applied to each paddock × variable series:

  1. Split out non-time-dependent vars (static metadata, etc.).
  2. Resample time-dependent data with a days-day median.
  3. Fill gaps with PCHIP (monotone cubic) interpolation; fall back to mean-fill for series with fewer than 2 valid points.
  4. Smooth with a Savitzky-Golay filter (window_length and polyorder configurable).
  5. Re-attach static variables and persist as Zarr v2 to {paddocks_filepath stem}_timeseries_smoothed.zarr.

Parameters:

Name Type Description Default
troi

The :class:troi.Troi.

required
ds_paddockTS

Optional in-memory paddockTS dataset. If None, opens (or generates, then opens) the cached timeseries zarr.

None
paddocks_filepath

Path to the paddocks GeoPackage. Used to derive the timeseries zarr path. If None, defaults to Paths(troi).sam_paddocks.

None
days

Resampling cadence in days. Default 10.

10
window_length

Savitzky-Golay window size in number of resampled samples. Coerced to the next odd integer if even and clipped to fit short series. Default 7.

7
polyorder

Savitzky-Golay polynomial order. Must be less than window_length. Default 2.

2

Returns:

Type Description

xarray.Dataset: Smoothed dataset on dims (paddock, time)

with the same data variables as the input, plus an observed

boolean variable marking which resampled bins contained at least

one real observation (False = gap-filled). Also persisted to

{paddocks_filepath stem}_timeseries_smoothed.zarr.

split_paddock_time_series_by_year

PaddockTS.Phenology.make_yearly_paddock_time_series.split_paddock_time_series_by_year

split_paddock_time_series_by_year(ds)

Group a paddock time-series dataset into one slice per calendar year.

Adds a doy coordinate (day-of-year, 1–366) on the time dimension of each per-year slice so seasonal curves can be aligned across years on a common DOY axis.

Parameters:

Name Type Description Default
ds

An xarray.Dataset on dims (paddock, time) (typically the output of :func:make_paddock_time_series).

required

Returns:

Type Description

dict[int, xarray.Dataset]: Mapping {year: ds_year} where each

ds_year covers a single calendar year and carries a doy

coordinate alongside time.