Merging Columns

Tables often contain related data spread across multiple columns that would be better presented as a single, combined value. For example, a measurement and its uncertainty, a low and high bound forming a range, or a count with its corresponding percentage. The cols_merge*() family of methods combines the content of two or more columns into one, giving you a more compact and readable table.

Setting Up the Example Data

We will use a small dataset that includes a measurement with uncertainty, a range, and a count with a percentage.

import pandas as pd
from great_tables import GT

experiment_df = pd.DataFrame({
    "trial": ["Trial 1", "Trial 2", "Trial 3", "Trial 4"],
    "measurement": [12.45, 8.92, 15.03, None],
    "uncertainty": [0.32, 0.15, 0.48, None],
    "low": [10.5, 7.8, 13.2, 9.1],
    "high": [14.2, 10.1, 16.8, 12.5],
    "n_obs": [120, 85, 200, 0],
    "pct": [34.2, 24.3, 57.1, 0.0],
})

gt_tbl = GT(experiment_df, rowname_col="trial")
gt_tbl
measurement uncertainty low high n_obs pct
Trial 1 12.45 0.32 10.5 14.2 120 34.2
Trial 2 8.92 0.15 7.8 10.1 85 24.3
Trial 3 15.03 0.48 13.2 16.8 200 57.1
Trial 4 9.1 12.5 0 0.0

With six data columns, this table is quite wide. Let’s reduce the column count by merging related pairs together.

Merging with a Pattern

The most general method is cols_merge(). It takes a columns= list where the first column becomes the target (the one that receives the merged content), and a pattern= string that controls how column values are combined. In the pattern, {0} refers to the first column, {1} to the second, and so on.

(
    GT(experiment_df, rowname_col="trial")
    .cols_merge(
        columns=["measurement", "uncertainty"],
        pattern="{0} ± {1}"
    )
)
measurement low high n_obs pct
Trial 1 12.45 ± 0.32 10.5 14.2 120 34.2
Trial 2 8.92 ± 0.15 7.8 10.1 85 24.3
Trial 3 15.03 ± 0.48 13.2 16.8 200 57.1
Trial 4 NA ± NA 9.1 12.5 0 0.0

The measurement column now contains the merged text and the uncertainty column is automatically hidden. This hiding behavior can be controlled with the hide_columns= argument.

Conditional Content with <<>>

Sometimes a column value may be missing, and you want the merged text to adapt gracefully. Wrapping part of the pattern in double angle brackets (<<...>>) makes that section conditional: it will be omitted entirely if any referenced column value inside is missing.

(
    GT(experiment_df, rowname_col="trial")
    .cols_merge(
        columns=["measurement", "uncertainty"],
        pattern="{0}<< ± {1}>>"
    )
)
measurement low high n_obs pct
Trial 1 12.45 ± 0.32 10.5 14.2 120 34.2
Trial 2 8.92 ± 0.15 7.8 10.1 85 24.3
Trial 3 15.03 ± 0.48 13.2 16.8 200 57.1
Trial 4 NA 9.1 12.5 0 0.0

In this example, Trial 4 has missing values for both columns. The conditional section << ± {1}>> is dropped when uncertainty is missing, producing a cleaner result than showing placeholder text.

Merging Value and Uncertainty

The cols_merge_uncert() method is a convenience wrapper specifically designed for the common pattern of a measurement paired with its uncertainty. It handles missing values automatically and renders the separator as a proper plus-minus sign.

(
    GT(experiment_df, rowname_col="trial")
    .cols_merge_uncert(col_val="measurement", col_uncert="uncertainty")
)
measurement low high n_obs pct
Trial 1 12.45 ± 0.32 10.5 14.2 120 34.2
Trial 2 8.92 ± 0.15 7.8 10.1 85 24.3
Trial 3 15.03 ± 0.48 13.2 16.8 200 57.1
Trial 4 9.1 12.5 0 0.0

The sep= argument controls the text between the value and uncertainty (defaulting to " ± "). You can combine this with fmt_number() to control the decimal precision of the merged values.

(
    GT(experiment_df, rowname_col="trial")
    .fmt_number(columns=["measurement", "uncertainty"], decimals=1)
    .cols_merge_uncert(col_val="measurement", col_uncert="uncertainty")
)
measurement low high n_obs pct
Trial 1 12.4 ± 0.3 10.5 14.2 120 34.2
Trial 2 8.9 ± 0.1 7.8 10.1 85 24.3
Trial 3 15.0 ± 0.5 13.2 16.8 200 57.1
Trial 4 9.1 12.5 0 0.0

Formatting should be applied before merging, since the merge operates on the already-formatted text content.

Merging a Range

The cols_merge_range() method combines two columns into a range display, separated by an en dash by default. This is ideal for confidence intervals, date ranges, or any pair of low/high bounds.

(
    GT(experiment_df, rowname_col="trial")
    .cols_merge_range(col_begin="low", col_end="high")
)
measurement uncertainty low n_obs pct
Trial 1 12.45 0.32 10.5–14.2 120 34.2
Trial 2 8.92 0.15 7.8–10.1 85 24.3
Trial 3 15.03 0.48 13.2–16.8 200 57.1
Trial 4 9.1–12.5 0 0.0

You can customize the separator with sep=. The special values "--" and "---" are automatically rendered as an en dash and em dash, respectively. Any other string is used literally.

(
    GT(experiment_df, rowname_col="trial")
    .cols_merge_range(col_begin="low", col_end="high", sep=" to ")
)
measurement uncertainty low n_obs pct
Trial 1 12.45 0.32 10.5 to 14.2 120 34.2
Trial 2 8.92 0.15 7.8 to 10.1 85 24.3
Trial 3 15.03 0.48 13.2 to 16.8 200 57.1
Trial 4 9.1 to 12.5 0 0.0

Merging Count and Percentage

When you have a count column alongside a pre-computed percentage column, cols_merge_n_pct() merges them into a format like "120 (34.2%)". This is a common pattern in statistical tables and survey results.

(
    GT(experiment_df, rowname_col="trial")
    .cols_merge_n_pct(col_n="n_obs", col_pct="pct")
)
measurement uncertainty low high n_obs
Trial 1 12.45 0.32 10.5 14.2 120 (34.2)
Trial 2 8.92 0.15 7.8 10.1 85 (24.3)
Trial 3 15.03 0.48 13.2 16.8 200 (57.1)
Trial 4 9.1 12.5 0

Notice that Trial 4 shows "0" without a percentage. This is intentional: when the count is zero, showing "0 (0.0%)" would be redundant, so the method displays only the count.

Combining Multiple Merges

You can apply several merge operations in the same table to consolidate all related column pairs at once.

(
    GT(experiment_df, rowname_col="trial")
    .fmt_number(columns=["measurement", "uncertainty", "low", "high"], decimals=1)
    .cols_merge_uncert(col_val="measurement", col_uncert="uncertainty")
    .cols_merge_range(col_begin="low", col_end="high")
    .cols_merge_n_pct(col_n="n_obs", col_pct="pct")
    .cols_label(
        measurement="Result",
        low="Range",
        n_obs="Observations"
    )
)
Result Range Observations
Trial 1 12.4 ± 0.3 10.5–14.2 120 (34.2)
Trial 2 8.9 ± 0.1 7.8–10.1 85 (24.3)
Trial 3 15.0 ± 0.5 13.2–16.8 200 (57.1)
Trial 4 9.1–12.5 0

By merging three pairs of columns, we reduced the table from six data columns down to three, each conveying the same information in a more compact form.

Column merging is a powerful technique for building information-dense tables. By combining related values into unified columns, you reduce visual clutter and help readers process the data more efficiently. The specialized cols_merge_uncert(), cols_merge_range(), and cols_merge_n_pct() methods handle the most common patterns, while cols_merge() with its pattern syntax gives you full flexibility for any custom arrangement.