Skip to content

feat(postprocessing): teach pivot() to compute percent-of-row/col/total (#42809) - #42976

Open
Abdulrehman-PIAIC80387 wants to merge 2 commits into
apache:masterfrom
Abdulrehman-PIAIC80387:fix/pivot-pandas-show-values-as-42809
Open

feat(postprocessing): teach pivot() to compute percent-of-row/col/total (#42809)#42976
Abdulrehman-PIAIC80387 wants to merge 2 commits into
apache:masterfrom
Abdulrehman-PIAIC80387:fix/pivot-pandas-show-values-as-42809

Conversation

@Abdulrehman-PIAIC80387

Copy link
Copy Markdown
Contributor

SUMMARY

First half of #42809. Extends superset/utils/pandas_postprocessing/pivot.py with an optional show_values_as argument that computes percent-of-row, percent-of-column, or percent-of-grand-total after pivoting, mirroring the client-side fractionOf semantic in react-pivottable/utilities.ts:739 (hardened by @rusackas in #42810).

WHY THIS IS HALF THE FIX

#42809 has two halves:

  1. Server-side capabilitypivot() needs to know how to compute percentages. This PR.
  2. Frontend wiring — the pivot chart's buildQuery.ts needs to include pivot postprocessing (with show_values_as) in the query so CSV/XLSX exports pick it up. Follow-up PR — needs a design decision on whether to emit the operation always (client would need to tolerate pre-pivoted DataFrames) or only for export flows (result_type in {CSV, XLSX}).

@rusackas / @sadpandajoe — happy to fold Path B into this PR if you have a preferred design, or land this first and open the follow-up. Let me know which shape you want.

FIX

  • Adds show_values_as param on pivot(), accepting percent_row, percent_col, percent_total, or None / "actual" (no-op).
  • Post-pivot, when the mode is set, each cell is divided by the appropriate rollup total.
  • On a multi-metric pivot (MultiIndex columns) the totals are computed within each metric group — never across metrics — so metric A's percentages are never contaminated by metric B's values (matches the client's metricAxis handling).
  • Default is None — no behavior change for existing callers of pivot postprocessing (echarts Timeseries, BigNumber, MixedTimeseries).

EDGE CASES (mirror #42810's client-side guards)

Guard Behavior
NaN/NULL numerator Stays NaN (renders blank) — a genuine SQL NULL does not become 0.0%
Zero row/column denominator Cells for that row/column become NaN, not Infinity
Zero grand total All cells become NaN
Invalid show_values_as value Raises InvalidPostProcessingError (no silent no-op)

BEHAVIOR MATRIX

Mode Effect
None / "actual" pivoted DataFrame unchanged (no-op) — default
"percent_row" each cell = cell / row-total
"percent_col" each cell = cell / column-total
"percent_total" each cell = cell / grand-total

TESTING INSTRUCTIONS

pytest tests/unit_tests/pandas_postprocessing/test_pivot.py -v -k show_values_as

Eight new tests: actual-is-noop, percent_row, percent_col, percent_total, NaN-numerator-preserved, zero-grand-total-produces-NaN, multi-metric-keeps-metrics-separate, invalid-mode-raises.

ADDITIONAL INFORMATION

Extends ``superset/utils/pandas_postprocessing/pivot.py`` with an
optional ``show_values_as`` argument that expresses each metric cell
as a fraction of the row / column / grand total after pivoting,
mirroring the pivot chart's client-side ``fractionOf`` semantic in
``plugin-chart-pivot-table/src/react-pivottable/utilities.ts:739`` so
server-side rendering paths (CSV / XLSX exports, scheduled reports)
can eventually reproduce the browser output.

Values:

- ``percent_row``:   cell / row-total     (denominator sums across columns)
- ``percent_col``:   cell / column-total  (denominator sums across rows)
- ``percent_total``: cell / grand-total   (denominator sums the DataFrame)
- ``None`` / ``"actual"``: no-op (default) — no behavior change for existing
  callers of ``pivot`` postprocessing (echarts Timeseries, BigNumber, etc.)

Edge cases mirror the client-side apache#42810 guards:

- NaN/NULL numerator stays NaN — a genuine SQL NULL renders blank rather
  than a measured "0.0%".
- Zero or NaN denominator produces NaN cells rather than Infinity from
  division-by-zero.
- On a multi-metric pivot (MultiIndex columns) the totals are computed
  *within each metric group* so metric A's percentages are never
  contaminated by metric B's values.

This is the server-side foundation for apache#42809. The pivot chart's
frontend ``buildQuery.ts`` still needs to add ``pivot`` postprocessing
with ``show_values_as`` so exports pick it up — that is a separate PR
because it requires a design decision on whether to emit the operation
for all query paths or only for export-format queries (client tolerance
for a pre-pivoted DataFrame).

Refs apache#42809
@dosubot dosubot Bot added the viz:charts:pivot Related to the Pivot Table charts label Aug 10, 2026
@bito-code-review

bito-code-review Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #f9d2c3

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: ce172a1..ce172a1
    • superset/utils/pandas_postprocessing/pivot.py
    • tests/unit_tests/pandas_postprocessing/test_pivot.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment thread superset/utils/pandas_postprocessing/pivot.py Outdated
Comment thread superset/utils/pandas_postprocessing/pivot.py Outdated
Comment on lines +564 to +570
result = pivot(
df=df,
index=["row"],
columns=["col"],
aggregates={"v": {"operator": "sum"}},
show_values_as="percent_row",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: This regression test cannot exercise NaN numerator preservation with operator='sum': pandas skips NaN values and returns zero for the all-NaN pivot cell, so the value is no longer NaN when _div_preserving_nan runs. The assertion that the cell remains NaN therefore fails (or, if the aggregation behavior changes, does not validate the intended SQL NULL case). Use an aggregation/input fixture that preserves the missing value before the percentage transform. [logic error]

Severity Level: Major ⚠️
- ❌ The new regression test fails under the configured sum aggregator.
- ⚠️ NaN-preservation behavior remains unverified.
- ⚠️ CI cannot validate the intended NULL rendering contract.

Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/pandas_postprocessing/test_pivot.py
**Line:** 564:570
**Comment:**
	*Logic Error: This regression test cannot exercise NaN numerator preservation with `operator='sum'`: pandas skips NaN values and returns zero for the all-NaN pivot cell, so the value is no longer NaN when `_div_preserving_nan` runs. The assertion that the cell remains NaN therefore fails (or, if the aggregation behavior changes, does not validate the intended SQL NULL case). Use an aggregation/input fixture that preserves the missing value before the percentage transform.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 80585a9678. Verified empirically that pd.Series([np.nan]).sum(skipna=True) == 0.0, so the previous fixture never actually produced a NaN pivot cell. Rewrote the fixture to omit the (r1, c2) row entirely — pivot_table then fills that cell with a genuine NaN, which does exercise _div_preserving_nan's NaN-preservation path. Test asserts both that the NaN cell stays NaN AND that the surviving cell divides against just its own value (r1 c1 == 1.0 since the row-total after skipna is 10, not 10+0).

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 14.63415% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.06%. Comparing base (3539c41) to head (80585a9).
⚠️ Report is 51 commits behind head on master.

Files with missing lines Patch % Lines
superset/utils/pandas_postprocessing/pivot.py 14.63% 35 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (3539c41) and HEAD (80585a9). Click for more details.

HEAD has 62 uploads less than BASE
Flag BASE (3539c41) HEAD (80585a9)
python 50 2
presto 8 1
hive 8 1
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #42976      +/-   ##
==========================================
- Coverage   66.41%   57.06%   -9.36%     
==========================================
  Files        2858     2861       +3     
  Lines      161446   161764     +318     
  Branches    37190    37264      +74     
==========================================
- Hits       107222    92305   -14917     
- Misses      52187    68598   +16411     
+ Partials     2037      861    -1176     
Flag Coverage Δ
hive 38.14% <14.63%> (-0.07%) ⬇️
mysql ?
postgres ?
presto 40.10% <14.63%> (-0.08%) ⬇️
python 40.15% <14.63%> (-19.06%) ⬇️
sqlite ?
unit ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds server-side support for pivot-table “Show values as” percentage modes in the pandas postprocessing pivot() operator, so non-browser rendering paths (CSV/XLSX exports, scheduled reports) can match the pivot table’s client-side fraction display semantics.

Changes:

  • Extend pivot() with an optional show_values_as argument to compute percent-of-row/column/grand-total after pivoting, including multi-metric isolation.
  • Add unit tests covering the new percentage modes and key edge cases (NaN numerator preservation, zero grand total, invalid mode).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
superset/utils/pandas_postprocessing/pivot.py Implements show_values_as percent-of-row/col/total post-pivot transforms and validates mode values.
tests/unit_tests/pandas_postprocessing/test_pivot.py Adds regression tests for show_values_as behaviors and edge cases.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread superset/utils/pandas_postprocessing/pivot.py Outdated
Comment on lines +626 to +630
def test_pivot_show_values_as_invalid_mode_raises() -> None:
"""An unknown ``show_values_as`` value raises ``InvalidPostProcessingError``
rather than silently falling through to a no-op."""
with pytest.raises(InvalidPostProcessingError):
pivot(
Per review on apache#42976:

1. **marginal_distributions guard** — combining ``show_values_as`` with
   ``marginal_distributions`` would include the ``All`` margin row/col
   in the row/col/grand-total denominators, producing wrong percentages.
   Raise ``InvalidPostProcessingError`` explicitly rather than silently
   returning wrong numbers. Combining the two needs a first-class design
   (probably compute on the non-margin subset then re-insert the
   margins), out of scope here.

2. **Flat-multi-metric percent_total** — a multi-metric pivot with no
   ``columns`` groupby produces a flat column index where each column
   IS a metric. Verified with pandas 2.3.3 that the previous code summed
   across metrics for the grand total (metric a's magnitude leaking
   into metric b's percentages). Now iterate each column as its own
   single-metric block, matching the MultiIndex per-metric semantics.

3. **NaN numerator test was broken** — the previous fixture used
   ``operator="sum"`` on a value that included ``NaN``; ``pandas``
   ``.sum(skipna=True)`` on a single-element ``[NaN]`` group returns
   ``0.0``, not ``NaN``, so the test never actually exercised
   ``_div_preserving_nan``'s NaN-preservation path. Rewrote the fixture
   to use a missing (row, col) combination, which ``pivot_table`` fills
   with a genuine ``NaN`` cell — verified empirically.

4. **Falsy-string validation** — the previous
   ``if show_values_as and show_values_as != "actual"`` guard skipped
   validation for empty strings, silently no-oping bad input. Now
   an explicit sentinel check ``if show_values_as not in (None, "",
   "actual")`` rejects unknown modes uniformly and lets both ``None``
   and ``""`` route to the no-op path.

5. **New zero-row/col-denominator tests** — pin the guard against
   division-by-zero producing ``Infinity`` in row/column modes (already
   handled for grand-total mode). Explicit tests for both axes.

6. **Pandas-3 compat (found while fixing)** — ``df.groupby(level=0,
   axis=1)`` is deprecated in pandas 2.3 (FutureWarning) and removed in
   pandas 3.x. Refactored to iterate ``columns.get_level_values(0)``
   explicitly with ``df.xs``, avoiding the deprecated call and keeping
   the fix forward-compatible.

All six fix paths verified against pandas 2.3.3 with a standalone repro
script before writing tests, mirroring the process discipline learned
from apache#42793.
@netlify

netlify Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 80585a9
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a7ad0ee0d561400083539b6
😎 Deploy Preview https://deploy-preview-42976--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L viz:charts:pivot Related to the Pivot Table charts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants