Skip to content

feat(duckdb-node): histogram summary stat (numeric .01–.99 + categorical) - #939

Merged
paddymul merged 4 commits into
mainfrom
feat/duckdb-node-histograms
Jun 24, 2026
Merged

feat(duckdb-node): histogram summary stat (numeric .01–.99 + categorical)#939
paddymul merged 4 commits into
mainfrom
feat/duckdb-node-histograms

Conversation

@paddymul

Copy link
Copy Markdown
Collaborator

Adds the histogram summary stat to buckaroo-duckdb-node, the fast-follow #936 reserved (stats.ts: "Histograms/quantile displayers are a fast-follow"). A DuckDB column now renders the same histogram bars as the pandas/polars backends, through the existing displayer: 'histogram' pinned row.

Stacked on #936. Base is feat/930-duckdb-node-backend because the package only exists there. Once #936 merges, this retargets to main (automatic when the base branch is deleted).

What's here

  • histogram.ts — a faithful TS port of the Python producer (customizations/histogram.py):
    • Numeric: clips the meat to the 1st/99th percentile (np.quantile ≡ DuckDB quantile_cont), 10 bins, low/high tail markers, NA bucket.
    • Categorical: top-7 categories (cat_pop), then longtail / unique / NA.
    • 0–100 scale, numpy round-half-to-even, SI-prefix bucket labels.
  • histogramSql.ts — per-column quantile + equal-width-bin SQL (DuckDB has no width_bucket, so floor((v-min)/width) clamped to the last bin reproduces np.histogram's edges) and the top-N value-counts query, plus the numeric-vs-categorical dispatcher matching histogram.py:histogram. A failure on any single column yields an empty histogram, never a broken initial_state.
  • DuckSource.queryRows seam (implemented in the node-api adapter) for the small scalar aggregates; the no-coercion row path is untouched.
  • backend injects the histogram row right after dtype; columnConfig pins it with the histogram displayer (DefaultMainStyling.pinned_rows parity).

Validation

The real-DuckDB spike reproduces numpy's exact normalized meat populations for the Python INT_ARR fixture (1st/99th-percentile clipping verified end to end) plus a categorical column. Note CI runs SKIP_DUCKDB=1, so the spike runs locally — full suite (77 tests incl. spike) green locally; the non-DuckDB tests run on CI.

TDD

First commit pushes the contract tests red (they reference the not-yet-added modules); the second commit makes them green.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1dc5dfedf

ℹ️ 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".

categoricalHistogram,
buildHistogram,
type NumericHistogramArgs,
} from '../src/histogram';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the histogram source modules before importing them

These new tests import ../src/histogram, but the commit does not add packages/buckaroo-duckdb-node/src/histogram.ts (nor the histogramSql.ts module imported by the companion test/spike), and the current src tree only contains the existing backend/config/query files. In any environment with the package dev dependencies installed, Vitest will fail module resolution before executing the suite, so CI for buckaroo-duckdb-node is blocked until the histogram implementation files and the related DuckSource.queryRows/pinned-row changes these tests expect are included.

Useful? React with 👍 / 👎.

paddymul and others added 2 commits June 23, 2026 19:11
…iling)

Add the histogram-stat contract tests ahead of the implementation:

- histogram.test.ts: pure math ported from the Python producer's fixtures
  (histogram_test.py / polars_categorical_histogram_test.py) — numpy
  round-half-to-even, SI-prefix bucket labels, numeric tail/meat bars, and
  the categorical top-N/longtail/unique/NA bars on the 0–100 scale.
- histogramSql.test.ts: the quantile/bin SQL builders and row parsers.
- backend/columnConfig: assert the injected `histogram` pinned row (after
  dtype, displayer 'histogram') and per-column bar lists.
- spike.duckdb.test.ts: real-DuckDB numeric histogram reproduces numpy's
  1st/99th-percentile normalized populations, plus a categorical column.

These reference src/histogram.ts + src/histogramSql.ts (not yet added), so
the suite is red until the next commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…olumns

Compute the `histogram` bar list the viewer renders through the
`displayer: 'histogram'` pinned row, matching buckaroo's pandas/polars
producer (customizations/histogram.py) so a DuckDB column shows the same bars.

- histogram.ts: faithful TS port of the Python math — numeric path clips the
  meat to the 1st/99th percentile (np.quantile == quantile_cont), 10 bins,
  low/high tail markers and an NA bucket; categorical path takes the top-7
  categories plus longtail/unique/NA. All percentages 0–100, rounded with
  numpy round-half-to-even, SI-prefix bucket labels.
- histogramSql.ts: per-column quantile + equal-width-bin SQL (DuckDB has no
  width_bucket, so floor((v-min)/width) clamped to the last bin reproduces
  np.histogram's edges) and the top-N value-counts query, plus the dispatcher
  that picks numeric vs categorical exactly like histogram.py:histogram. A
  failure on any single column yields an empty histogram, never a broken
  initial_state.
- DuckSource gains a `queryRows` seam (implemented in the node-api adapter)
  for these small scalar aggregates; the no-coercion row path is untouched.
- backend injects the `histogram` row right after dtype; columnConfig pins it
  with the histogram displayer (DefaultMainStyling.pinned_rows parity).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@paddymul
paddymul changed the base branch from feat/930-duckdb-node-backend to main June 23, 2026 23:11
@paddymul
paddymul force-pushed the feat/duckdb-node-histograms branch from c1dc5df to 4d8b2a1 Compare June 23, 2026 23:11
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

📦 TestPyPI package published

pip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.1.dev28068689297

or with uv:

uv pip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.1.dev28068689297

MCP server for Claude Code

claude mcp add buckaroo-table -- uvx --from "buckaroo[mcp]==0.15.1.dev28068689297" --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo-table

📖 Docs preview

🎨 Storybook preview

@paddymul

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87fa4bbbc7

ℹ️ 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".

least(${lastBin}, greatest(0, floor((_meat.v - _mm.meat_min) / ((_mm.meat_max - _mm.meat_min) / ${MEAT_BINS}.0))))::INTEGER AS bin,
count(*) AS c
FROM _meat, _mm
WHERE _mm.meat_max > _mm.meat_min

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve numeric histograms for constant meat ranges

For skewed numeric columns where the 1–99% meat is non-empty but all the same value (for example, many copies of one center value plus enough repeated tail values to make distinct_count > 5), this filter drops the only aggregate row because meat_max === meat_min. computeColumnHistogram then sees no numeric args and falls back to a categorical histogram, whereas the Python/NumPy producer still emits a numeric histogram for constant meat by expanding the bin range. Special-case the equal-min/max case instead of treating it as unavailable.

Useful? React with 👍 / 👎.

…stogram

computeColumnHistogram reimplemented the numeric-vs-categorical dispatch
already in buildHistogram, so the two copies could drift and the production
numeric path was only exercised by the SKIP_DUCKDB-gated spike (never on CI).

Make buildHistogram async with lazy fetchers (fetchNumericArgs/
fetchCategorical) so it keeps the short-circuit — the categorical query runs
only when the numeric path doesn't win — and have computeColumnHistogram
delegate to it with SQL-backed fetchers. The CI-run histogram.test.ts now
drives the exact dispatcher that ships, and asserts the categorical fetch is
skipped when numeric wins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aframe tests

A DOUBLE column containing nan/±inf crashed initialState: DuckDB's SUMMARIZE
runs STDDEV_SAMP, which throws "out of range" on any non-finite value and
aborts the whole stats query (ddd_library.py:df_with_infinity, which pandas
renders fine).

Add RenamePlan.statsRelation — renamedRelation with non-finite floats nulled
(CASE WHEN isfinite(col) ...) — and summarize over it. The rule: non-finite
floats are missing for summary statistics. Only the stats path is guarded; the
row (COPY) and histogram paths stay no-coercion. Real IEEE-754 types only;
DECIMAL is fixed-point and can't be non-finite, so it's left untouched.

Also add test/ddd.duckdb.test.ts porting the ddd pathologies that map to SQL
columns (column named "index", ±inf/nan, HUGEINT > 2^63 lossiness, the
ENUM/INTERVAL/TIME/DECIMAL/BLOB weird-types frame), plus CI-running unit tests
for statsRelation (the ddd suite is SKIP_DUCKDB and doesn't gate CI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@paddymul
paddymul added this pull request to the merge queue Jun 24, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 24, 2026
@paddymul
paddymul added this pull request to the merge queue Jun 24, 2026
Merged via the queue into main with commit 5f1b102 Jun 24, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant