Summary Rows

Summary rows provide aggregated values (such as totals, means, or counts) directly in the table, adjacent to the data they summarize. Great Tables supports two types: group-level summaries that appear next to each row group, and grand summaries that aggregate across the entire table. Both types let you define multiple aggregation functions at once and control where the summary appears.

Setting Up the Example Data

For these examples, we will use a sales dataset with row groups representing different product categories.

import polars as pl
from great_tables import GT

sales_df = pl.DataFrame({
    "product": ["Laptop", "Mouse", "Keyboard", "Monitor", "Webcam", "Headset"],
    "category": ["Computing", "Computing", "Computing", "Peripherals", "Peripherals", "Peripherals"],
    "units_sold": [45, 230, 180, 65, 120, 95],
    "revenue": [67500, 4600, 9000, 19500, 6000, 7125],
})

gt_sales = (
    GT(sales_df, rowname_col="product", groupname_col="category")
    .tab_header(title="Q4 Product Sales", subtitle="By category")
    .fmt_number(columns="revenue", decimals=0, use_seps=True)
)

gt_sales
Q4 Product Sales
By category
units_sold revenue
Computing
Laptop 45 67,500
Mouse 230 4,600
Keyboard 180 9,000
Peripherals
Monitor 65 19,500
Webcam 120 6,000
Headset 95 7,125

This table has two row groups: "Computing" and "Peripherals". We can now add summaries at the group level and at the grand level.

Group-Level Summary Rows

The summary_rows() method adds summary rows to each row group. You provide aggregation functions through the fns= argument as a dictionary, where keys become the summary row labels and values are the aggregation logic.

When using a Polars DataFrame, the aggregation values should be Polars expressions.

(
    gt_sales
    .summary_rows(
        fns={"Total": pl.col("units_sold", "revenue").sum()}
    )
)
Q4 Product Sales
By category
units_sold revenue
Computing
Laptop 45 67,500
Mouse 230 4,600
Keyboard 180 9,000
Total 455 81100
Peripherals
Monitor 65 19,500
Webcam 120 6,000
Headset 95 7,125
Total 280 32625

Each row group now has a "Total" summary row at the bottom showing the sum of numeric columns within that group.

Multiple Aggregation Functions

You can include several functions in the fns= dictionary to produce multiple summary rows per group.

(
    gt_sales
    .summary_rows(
        fns={
            "Total": pl.col("units_sold", "revenue").sum(),
            "Average": pl.col("units_sold", "revenue").mean(),
        }
    )
)
Q4 Product Sales
By category
units_sold revenue
Computing
Laptop 45 67,500
Mouse 230 4,600
Keyboard 180 9,000
Total 455 81100
Average 151.66666666666666 27033.333333333332
Peripherals
Monitor 65 19,500
Webcam 120 6,000
Headset 95 7,125
Total 280 32625
Average 93.33333333333333 10875.0

Both a "Total" and an "Average" row now appear at the bottom of each group.

Placing Summaries at the Top

By default, summary rows appear at the bottom of each group. You can place them at the top instead by setting side="top".

(
    gt_sales
    .summary_rows(
        fns={"Total": pl.col("units_sold", "revenue").sum()},
        side="top"
    )
)
Q4 Product Sales
By category
units_sold revenue
Computing
Total 455 81100
Laptop 45 67,500
Mouse 230 4,600
Keyboard 180 9,000
Peripherals
Total 280 32625
Monitor 65 19,500
Webcam 120 6,000
Headset 95 7,125

The summary row now sits above the data rows in each group rather than below them, making the totals immediately visible.

Targeting Specific Groups

If you only want summaries for certain groups, use the groups= argument with a list of group names.

(
    gt_sales
    .summary_rows(
        fns={"Total": pl.col("units_sold", "revenue").sum()},
        groups=["Computing"]
    )
)
Q4 Product Sales
By category
units_sold revenue
Computing
Laptop 45 67,500
Mouse 230 4,600
Keyboard 180 9,000
Total 455 81100
Peripherals
Monitor 65 19,500
Webcam 120 6,000
Headset 95 7,125

Only the "Computing" group receives a summary row.

Grand Summary Rows

The grand_summary_rows() method works the same way as summary_rows(), but it aggregates across all data in the table regardless of row groups. The resulting summary rows appear at the very bottom (or top) of the table.

(
    gt_sales
    .grand_summary_rows(
        fns={"Grand Total": pl.col("units_sold", "revenue").sum()}
    )
)
Q4 Product Sales
By category
units_sold revenue
Computing
Laptop 45 67,500
Mouse 230 4,600
Keyboard 180 9,000
Peripherals
Monitor 65 19,500
Webcam 120 6,000
Headset 95 7,125
Grand Total 735 113725

A single "Grand Total" row appears below all row groups, showing the overall totals.

Combining Group and Grand Summaries

You can use both summary_rows() and grand_summary_rows() on the same table to provide aggregation at both levels.

(
    gt_sales
    .summary_rows(
        fns={"Subtotal": pl.col("units_sold", "revenue").sum()}
    )
    .grand_summary_rows(
        fns={
            "Grand Total": pl.col("units_sold", "revenue").sum(),
            "Overall Average": pl.col("units_sold", "revenue").mean(),
        }
    )
)
Q4 Product Sales
By category
units_sold revenue
Computing
Laptop 45 67,500
Mouse 230 4,600
Keyboard 180 9,000
Subtotal 455 81100
Peripherals
Monitor 65 19,500
Webcam 120 6,000
Headset 95 7,125
Subtotal 280 32625
Grand Total 735 113725
Overall Average 122.5 18954.166666666668

Each group now has a "Subtotal" row, and the table finishes with a "Grand Total" and "Overall Average" row that span across all groups.

Working with Pandas DataFrames

When using a Pandas DataFrame, the aggregation functions receive a Pandas DataFrame and should work accordingly.

import pandas as pd

sales_pd = pd.DataFrame({
    "product": ["Laptop", "Mouse", "Keyboard", "Monitor", "Webcam", "Headset"],
    "category": ["Computing", "Computing", "Computing", "Peripherals", "Peripherals", "Peripherals"],
    "units_sold": [45, 230, 180, 65, 120, 95],
    "revenue": [67500, 4600, 9000, 19500, 6000, 7125],
})

(
    GT(sales_pd, rowname_col="product", groupname_col="category")
    .fmt_number(columns="revenue", decimals=0, use_seps=True)
    .grand_summary_rows(
        fns={"Total": lambda df: df.sum(numeric_only=True)}
    )
)
units_sold revenue
Computing
Laptop 45 67,500
Mouse 230 4,600
Keyboard 180 9,000
Peripherals
Monitor 65 19,500
Webcam 120 6,000
Headset 95 7,125
Total 735 113725

The numeric_only=True argument ensures that only numeric columns are summed, avoiding errors with string columns.

Styling Summary Rows

Summary rows can be styled using loc.grand_summary() and loc.summary() with tab_style(). This lets you visually distinguish summary rows from data rows.

from great_tables import loc, style

(
    gt_sales
    .grand_summary_rows(
        fns={"Grand Total": pl.col("units_sold", "revenue").sum()}
    )
    .tab_style(
        style=style.fill(color="lightyellow"),
        locations=loc.grand_summary()
    )
)
Q4 Product Sales
By category
units_sold revenue
Computing
Laptop 45 67,500
Mouse 230 4,600
Keyboard 180 9,000
Peripherals
Monitor 65 19,500
Webcam 120 6,000
Headset 95 7,125
Grand Total 735 113725

Summary rows are a natural companion to row groups, providing the aggregated context that readers need to interpret grouped data. By combining group-level and grand summaries, formatting, and targeted styling, you can build tables that tell a complete analytical story.