Replace searchsorted with a hash index in the reverse-pivot scatter - #3
Replace searchsorted with a hash index in the reverse-pivot scatter#3Mmoncadaisla wants to merge 2 commits into
Conversation
The reverse pivot resolves each result row's dim-coord values to array positions before scatter-writing into the dense output. Irregular (non-uniformly-spaced) axes previously used np.argsort once plus np.searchsorted per batch: O(log n) per row. They now use a pd.Index whose hash table is built once per dimension and probed per batch with get_indexer: O(1) amortized per row. Measured 2.1-3.3x faster reconstruction on shuffled irregular-axis results from 24K to 24M cells (the speedup grows with axis cardinality); uniformly spaced axes keep the existing affine fast path and regular-grid workloads (e.g. ERA5 lat/lon/time) are unaffected. The three strategies now live in one place, _CoordLookup: * affine formula for uniformly spaced axes (unchanged); * hash index for irregular axes with unique values; * the previous argsort+searchsorted for axes with duplicate values, which a unique-key hash table cannot represent. A result value absent from the axis now raises a ValueError naming the dimension (previously a searchsorted misplacement surfaced as an opaque AssertionError or a silent wrong-cell write). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8af8d7dd72
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…cate semantics - Tests now exercise each lookup strategy through the public to_dataset contract (values, dims, coords) on shuffled rows, with dims inferred from the template. Only two behaviors stay at the _scatter_batches_to_ndarray seam, with the reason documented: the missing-value error and the duplicate-axis fallback, both unreachable through the eager public path because to_dataset derives each axis from the same rows it scatters. - pd.Index construction falls back to sorted search for dtypes pandas cannot index (float16 raises NotImplementedError on pandas 2.3.0), preserving the previous behavior for those axes; regression-tested through to_dataset. - The _CoordLookup docstring no longer claims to_dataset raises on duplicate dim tuples (no reconstruction path does); it now describes the actual behavior: sorted-search resolution to one of the holding positions plus the scatter's last-write-wins overwrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@codex review |
|
Confirmed: full suite on a clean VM build at head |
|
Codex Review: Didn't find any major issues. More of your lovely PRs please. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Why
The reverse pivot (
to_dataset) resolves each result row's dim-coord values to array positions before scatter-writing into the dense output. Irregular (non-uniformly-spaced) axes — station networks, arbitrary point sets — usednp.argsortonce plusnp.searchsortedper batch: O(log n) per row. Every fast engine (DuckDB'sPhysicalPivot, ClickHouse's aggregator, Daft's pivot) does this same value-to-slot resolution with a hash table built once and probed vectorized; this PR adopts that discipline viapd.Index.get_indexer, whose persistent hash table is built once per dimension and probed per batch: O(1) amortized per row.Uniformly spaced axes keep the existing affine fast path, so regular-grid workloads (ERA5 lat/lon/time) are completely unaffected — verified: ordered/unordered ERA5 reconstruction ratios stay ~1.0 on DataFusion, DuckDB, and Polars.
Measured
Isolated reconstruction through
xql.to_dataseton shuffled rows over an irregular axis (GCP n2-standard-16, median of 5):Thread-scaling check (4 dask-style threads, 5 M probes over a 200 K-value axis): identical 3.7x scaling for both implementations — pandas' hash probe releases the GIL as well as
searchsorteddoes — with the hash path ~7x faster per call.What
The three position-resolution strategies now live in one place,
_CoordLookup, built once per dimension per reconstruction:pd.Index, built once,get_indexerper batch) for irregular axes with unique values;Behavior change: a result value absent from an irregular unique axis now raises
ValueErrornaming the dimension (previously asearchsortedmisplacement surfaced as an opaqueAssertionErrordeep in the scatter, or a silent wrong-cell write). Both real callers constructrequestedsuch that this cannot fire on well-formed results (_dataset_from_batchesderives coords viapd.uniqueof the same batches;SQLBackendArray._raw_getitemwindows come from the engine filter), so it only converts an existing latent corruption into a loud error.Tradeoff, quantified: the pandas hash table holds ~34 bytes/value transient vs ~16 bytes/value for the sorted copy it replaces (measured on a 2 M-value float64 axis) — per non-affine dimension, per reconstruction call, freed with the call frame. Affine axes build neither.
Validation
tests/test_coord_lookup.pypins each strategy on shuffled input, the descending-affine regression, NaN axis values, the duplicate-values fallback, the missing-value error, and an end-to-endto_datasetround-trip.ValueError) found no correctness refutation.mypyandruff check/formatmatchmain's baseline exactly.🤖 Generated with Claude Code