Back to Perspective

Grouping and Pivots

docs/md/explanation/view/config/grouping_and_pivots.md

5.2.014.0 KB
Original Source

Grouping and Pivots

Group By

A group by groups the dataset by the unique values of each column used as a group by - a close analogue in SQL to the GROUP BY statement. The underlying dataset is aggregated to show the values belonging to each group, and a total row is calculated for each group, showing the currently selected aggregated value (e.g. sum) of the column. Group by are useful for hierarchies, categorizing data and attributing values, i.e. showing the number of units sold based on State and City. In Perspective, group by are represented as an array of string column names to pivot, are applied in the order provided; For example, a group by of ["State", "City", "Postal Code"] shows the values for each Postal Code, which are grouped by City, which are in turn grouped by State.

<div class="javascript">
javascript
const view = await table.view({ group_by: ["a", "c"] });
</div> <div class="python">
python
view = table.view(group_by=["a", "c"])
</div> <div class="rust">
rust
let view = table.view(Some(ViewConfigUpdate {
    group_by: Some(vec!["a".into(), "c".into()]),
    ..ViewConfigUpdate::default()
})).await?;
</div>

group_rollup_mode

The group_rollup_mode option controls how the grouped rows themselves render:

  • "rollup" (the default) - the full hierarchy, with a subtotal row for every group at every level and a grand total row, each addressable by its __ROW_PATH__.
  • "flat" - leaf rows only, one row per deepest-level group, with no subtotal or grand total rows. Useful for chart plugins and exports where subtotal rows would double-count.
  • "total" - the grand total row only. "total" is mutually exclusive with group_by (which is cleared when it is set) - it is the one shape an empty group_by cannot express, since no group_by at all yields the unaggregated dataset.
<div class="javascript">
javascript
const view = await table.view({
    group_by: ["a"],
    group_rollup_mode: "flat",
});
</div> <div class="python">
python
view = table.view(group_by=["a"], group_rollup_mode="flat")
</div> <div class="rust">
rust
let view = table.view(Some(ViewConfigUpdate {
    group_by: Some(vec!["a".into()]),
    group_rollup_mode: Some(GroupRollupMode::Flat),
    ..ViewConfigUpdate::default()
})).await?;
</div>

Split By

A split by splits the dataset by the unique values of each column used as a split by. The underlying dataset is not aggregated, and a new column is created for each unique value of the split by. Each newly created column contains the parts of the dataset that correspond to the column header, i.e. a View that has ["State"] as its split by will have a new column for each state. In Perspective, Split By are represented as an array of string column names to pivot:

<div class="javascript">
javascript
const view = await table.view({ split_by: ["a", "c"] });
</div> <div class="python">
python
view = table.view(split_by=["a", "c"])
</div> <div class="rust">
rust
let view = table.view(Some(ViewConfigUpdate {
    split_by: Some(vec!["a".into(), "c".into()]),
    ..ViewConfigUpdate::default()
})).await?;
</div>

split_rollup_mode

The split_rollup_mode option is the split_by counterpart to group_rollup_mode, controlling whether subtotal column groups are emitted:

  • "flat" (the default) - only full-depth split combinations appear as columns, e.g. "CA|Sales". This is Perspective's historical behavior.
  • "rollup" - additionally emits a grand-total column per aggregate (named by the bare column name, e.g. "Sales", aggregating across every split group) and, when more than one split_by column is applied, a subtotal column per intermediate split group (e.g. "CA|Sales" alongside "CA|First Class|Sales"). Total and subtotal columns precede their groups, in pre-order.
<div class="javascript">
javascript
const view = await table.view({
    group_by: ["State"],
    split_by: ["Ship Mode"],
    split_rollup_mode: "rollup",
});
</div> <div class="python">
python
view = table.view(
    group_by=["State"],
    split_by=["Ship Mode"],
    split_rollup_mode="rollup",
)
</div> <div class="rust">
rust
let view = table.view(Some(ViewConfigUpdate {
    group_by: Some(vec!["State".into()]),
    split_by: Some(vec!["Ship Mode".into()]),
    split_rollup_mode: Some(SplitRollupMode::Rollup),
    ..ViewConfigUpdate::default()
})).await?;
</div>

Aggregates

Aggregates perform a calculation over an entire column, and are displayed when one or more Group By are applied to the View. Aggregates can be specified by the user, or Perspective will use the following sensible default aggregates based on column type:

  • "sum" for integer and float columns
  • "count" for all other columns

Perspective provides a selection of aggregate functions that can be applied to columns in the View constructor using a dictionary of column name to aggregate function name.

<div class="javascript">
javascript
const view = await table.view({
    aggregates: {
        a: "avg",
        b: "distinct count",
    },
});
</div> <div class="python">
python
view = table.view(
  aggregates={
    "a": "avg",
    "b": "distinct count"
  }
)
</div> <div class="rust">
rust
use std::collections::HashMap;
let view = table.view(Some(ViewConfigUpdate {
    aggregates: Some(HashMap::from([
        ("a".into(), "avg".into()),
        ("b".into(), "distinct count".into()),
    ])),
    ..ViewConfigUpdate::default()
})).await?;
</div>

Every aggregate is described below, grouped by what it computes. Which of them a given column accepts depends on its type — see Availability by column type.

Sums and products

AggregateDescriptionResult type
sumTotal of the group's valuesinteger or float
sum not nullAs sum, but non-finite (NaN) values are skipped rather than poisoning the totalinteger or float
sum absSum of the absolute values — Σ abs(v)integer or float
abs sumAbsolute value of the sum — abs(Σ v)integer or float
mulProduct of the group's valuesinteger or float
gmvGross market value — leaf rows are a plain sum, parent rows sum the absolute subtotal of each immediate child groupinteger or float
pct sum parentThe group's sum as a percentage of its parent row's, 0100; 100 at the root, and null when the parent's sum is 0float
pct sum totalThe group's sum as a percentage of the grand total, 0100float

A numeric aggregate widens to the input's numeric class — integer columns accumulate as integer, float columns as float.

Averages and dispersion

AggregateDescriptionResult type
avgArithmetic mean of the non-null valuesfloat
weighted meanΣ(value × weight) / Σ(weight), over rows where both the value and the weight are non-null and finite; null when the weights sum to 0. Takes a weight column as an argumentfloat
stddevPopulation standard deviationfloat
varPopulation variance — divides by N, not N - 1float

stddev and var are null for a group of fewer than two non-null values.

Extrema and order statistics

AggregateDescriptionResult type
min, maxSmallest and largest of the group's current valuesinput type
min by, max byThe value from the row at which a second column, supplied as an argument, is smallest or largestinput type
high, lowHigh and low water mark — the largest and smallest value this View has ever observed for the group, which never moves back when rows are updated or removedinput type
high minus lowmax - min of the group's current values, i.e. its range. Despite the name this uses min/max, not the water marksinput type
median, q1, q3The value at the 50%, 25% and 75% position of the group's values; on float columns an exact split averages the two adjacent valuesinput type

Positional

AggregateDescriptionResult type
firstValue from the group's earliest rowinput type
last by indexValue from the group's latest rowinput type
last minus firstlast by index minus firstinput type
lastValue from the group's most recently updated rowinput type

"Earliest" and "latest" are by the Table's index column, or by row order when the Table is unindexed. This is not the same as last, which tracks update recency rather than position.

Cardinality and identity

AggregateDescriptionResult type
countNumber of rows in the groupinteger
distinct countNumber of distinct values in the groupinteger
uniqueThe group's value when every row shares one, otherwise nullinput type
distinct leafAs unique, but only on leaf rows — parent rows are blankinput type
dominantThe most frequent non-null value, i.e. the mode; a tie resolves to whichever value reached the winning count firstinput type
anyThe group's first truthy value — any non-null value for string columns, the first non-zero for numbers and dates, the first true for boolean — or null if it has noneinput type
orIdentical to anyinput type
andtrue when every value in the group is truthy, else falseboolean
joinThe group's distinct values, sorted and rendered as a ", "-delimited string, truncated at 280 characters. Nulls render as nullstring

Nulls

Null handling is not uniform, and is usually what makes two similar-looking aggregates differ:

  • count counts rows, not values — a group of 3 rows whose value is null counts 3. This is not the same as the count window aggregate, which counts non-null values.
  • distinct count counts null as one distinct value, so a group of [1, null, null] counts 2.
  • sum, avg, stddev, var, dominant and weighted mean skip nulls entirely. avg divides by the count of non-null values, so a group of all nulls is null rather than 0.
  • any and or return the first truthy value, not the first non-null one — a numeric group of all 0, or a boolean group of all false, aggregates to null.
  • join renders nulls into its output as the literal text null.

Availability by column type

The aggregates a column accepts depend on its type:

Numeric columns (integer, float): sum, abs sum, sum abs, sum not null, mul, gmv, any, avg, mean, count, distinct count, distinct leaf, dominant, first, last, last by index, high, low, max, min, min by, max by, high minus low, last minus first, median, q1, q3, pct sum parent, pct sum total, stddev, var, unique, weighted mean.

String columns: count, any, distinct count, distinct leaf, dominant, first, last, last by index, join, median, q1, q3, unique, min by, max by.

Date/Datetime columns: count, any, avg, distinct count, distinct leaf, dominant, first, last, last by index, high, low, max, min, median, q1, q3, unique.

Boolean columns: count, any, and, or, distinct count, distinct leaf, dominant, first, last, last by index, unique.

<div class="warning"><code>avg</code> on a <code>date</code> or <code>datetime</code> column returns a <code>float</code> — the mean of the column's underlying numeric representation — not a date.</div>

Argument-taking aggregates

weighted mean, min by and max by each read a second column, and are written as a [name, [argument]] pair rather than a bare string:

<div class="javascript">
javascript
const view = await table.view({
    aggregates: { a: ["weighted mean", ["b"]] },
});
</div> <div class="python">
python
view = table.view(aggregates={"a": ("weighted mean", ["b"])})
</div> <div class="rust">
rust
let view = table.view(Some(ViewConfigUpdate {
    aggregates: Some(HashMap::from([(
        "a".into(),
        Aggregate::MultiAggregate("weighted mean".into(), vec!["b".into()]),
    )])),
    ..ViewConfigUpdate::default()
})).await?;
</div> <div class="warning">In Rust, <code>Aggregate::from(&amp;str)</code> splits on <code>" by "</code> to build a <code>MultiAggregate</code>. Single aggregates whose names contain that substring — <code>"last by index"</code> — must therefore be constructed as <code>Aggregate::SingleAggregate("last by index".into())</code> rather than <code>"last by index".into()</code>, which silently resolves to <code>last</code>.</div>

Aliases

Several aggregates answer to more than one name. Every name below is accepted anywhere an aggregate is, and each group refers to one function:

CanonicalAlso accepted
avgmean
distinct countdistinct, distinctcount, distinct_count
firstfirst by index
lastlast_value
highhigh_water_mark
lowlow_water_mark
pct sum totalpct sum grand total, pct_sum_grand_total
varvariance
stddevstandard deviation

Most multi-word aggregates also answer to a snake_case spelling — sum_not_null, sum_abs, abs_sum, weighted_mean, distinct_leaf, pct_sum_parent, pct_sum_total, min_by, max_by. Three do not, and are only accepted spelled with spaces: high minus low, last minus first and last by index.

A few names the engine parses are not implementedidentity, mean by count, and div/add, which have no way to receive their operands from a ViewConfig. Naming one is rejected exactly as a misspelled aggregate is: the View fails to construct with an error naming the aggregate and the column it was given for, and the Table is left untouched.