# Reshape uneven xarray time series into fixed windows without per-group loops

Reshape an irregularly sampled xarray time series into fixed (window, position_in_window) blocks using vectorized window labels (dt.floor), within-window positions from groupby(...).cumcount(), and set_index + unstack — no per-group slicing loops.

Exact reference: {"kind":"skill_version","skill_id":"skl_46RWqY80xzodxSWZeSAEhw","version_id":"skv_7z3VEWYSOzKS2QV4e4fWYQ"}

Applicability: [{"constraint":"2026.7.0","technology":"xarray","version_scheme":"exact"}]

# Reshape uneven xarray time series into fixed windows without per-group loops

## Problem

You have an xarray dataset with an unevenly spaced `time` coordinate (e.g. irregular sensor readings) and you want to reshape it into fixed-duration windows — a 2D array with dimensions `(window, position_in_window)` — so downstream code can treat each window as a row.

The common first attempt is a per-group loop: `ds.groupby(time.floored).map(some_slicing_fn)` or a manual slice-and-stack loop. This works but pays full Python overhead per group.

## Technique

Build the two MultiIndex levels as plain coordinate arrays, fully vectorized, then let `set_index` + `unstack` do the reshape in one shot:

1. **Window label**: `ds["time"].dt.floor("FREQ")` — the same bins `resample(time="FREQ")` would produce (when the frequency divides the day evenly; see caveat below).
2. **Within-window position**: `t.groupby(window.values).cumcount()` — gives 0..k-1 inside each window without ever materializing per-group slices.
3. Reshape with `set_index(time=("window", "pos"))` and `.unstack("time")`.

```python
t = ds["time"].to_series()
window = t.dt.floor("1800s")                  # same bins as resample(time="1800s")
pos = t.groupby(window.values).cumcount()     # 0..k-1 inside each window

out = (
    ds.assign(timestamp=("time", ds.time.values))  # keep original times through the unstack
      .assign_coords(window=("time", window.values), pos=("time", pos.to_numpy()))
      .set_index(time=("window", "pos"))
      .unstack("time")
)
```

`out` has dims `(window, pos)`, NaN-padded to the largest window.

## Measured results

Tested on a synthetic 200,000-point unevenly spaced series (xarray 2026.7.0):

- **0.13s** for the vectorized version above
- **28.28s** for the per-group `groupby(...).map` variant producing the identical reshape

The gap is entirely per-group Python overhead. Output correctness verified: all 200,000 values preserved per-window (checked first 200 windows), window labels from `dt.floor("1800s")` exactly equal `resample(time="1800s")` group keys, and original timestamps preserved through the unstack.

## Caveats

- `dt.floor` matches `resample` bins exactly only when the frequency divides the day evenly (e.g. `1800s`). For other frequencies, or a custom `origin`/`offset`, derive the labels from `resample(...).groups` keys instead of `dt.floor` so the binning stays identical.
- The reshape itself does not care about time encoding: `with_decoded_units` / `with_encoded_units`-style wrappers can stay as-is around this snippet.
- Windows shorter than the maximum are NaN-padded; use `dropna("pos", how="all")` on the result if the ragged edge matters to downstream code.


## Supporting basis and limitations

Verified against the tested draft at ~/workspace/goals/vectle-growth-strategy/tests/drafts/xarray-10919-answer.md (2026-09-17, xarray 2026.7.0, synthetic 200k-point uneven series). Draft verification: output dims correct (window, pos) with NaN padding; window labels from dt.floor("1800s") exactly equal resample(time="1800s") group keys; all 200,000 values preserved per-window (first 200 windows checked); original timestamps preserved through the unstack; timing 0.13s (vectorized) vs 28.28s (groupby.map variant) on identical data. Numbers, caveats (dt.floor matches resample bins only when the frequency divides the day evenly; otherwise derive labels from resample(...).groups keys), and code reproduced exactly from the draft.

## Change and rationale

New skill: vectorized reshaping of uneven xarray temporal data into fixed windows via dt.floor labels, groupby(...).cumcount() within-window positions, and set_index + unstack. Reproduces a genuinely tested draft: 0.13s vs 28.28s on a 200k-point uneven series under xarray 2026.7.0.

The per-group map/slice loop is the obvious approach to windowing uneven xarray series and it is ~217x slower than necessary (28.28s vs 0.13s on 200k points). The cumcount trick turns the reshape into pure vectorized index construction followed by a single unstack, a non-obvious idiom worth preserving as a reusable skill. Source discussion was live and unanswered.
