Don’t Show Me the Numbers! Play Suppression Whack-A-Mole in Tableau

Nicole Klassen
16 min readAug 30, 2023
Whack a mole game, what it feels like to implement complimentary suppression

As analysts, we all love our numbers. We clean them, analyze them, find interesting stories in them, and share them with our stakeholders. But we also understand the dangers in releasing certain numbers; especially small numbers that could allow end users to identify specific people. That’s where suppression comes in.

In theory, suppression sounds simple: If a number is less than X, don’t show it on your dashboard. And 90% of the time it is that easy. But then there are times when suppression in Tableau becomes a twisted mess of nested table calculations. So let’s look at some suppression examples and how to handle them. You can follow along by downloading the Excel files and Tableau workbook from my Google Drive.

For these examples, you first need to set up two parameters: Suppression String and Suppression Max. We’re going to use these instead of hardcoding suppression values to prepare for if/when your end user decides to change their mind. You know, even though they promised they wanted a “^” to indicate anything less than 10, now they think an “*” looks better and actually, anything under 20 needs suppressed, they can and will change their minds at the last minute. In that case, you can just update the parameter and not a million calculations. Here is how to set up those two parameters:

How to set up a string parameter to set the suppression character.
Setting up the string that will show if a value is suppressed
Setting up the string that will show if a value is suppressed
Setting up the parameter that dictates the values to suppress

Once you have the parameters created, there are a couple ways to incorporate them to suppress data.

The first way is the easiest: you have a single graph or column in a table, and you don’t need to show the grand totals:

Bar graph is row A suppressed
Only row A is suppressed, but since there are no grand totals you cannot figure out what that number is. To take the suppression one level further you could add a second calculation that makes the axis 0 if the label is suppressed, so you can’t even guess the number based on the length of the bar.

In the above bar graph, the requirements specify that anything less than 10 needs to be suppressed. Because you’re hiding the grand totals, you can just suppress anything less than 10 and you’re good to go. The formula for these is:

if [VALUE]<[Suppression Max] then [Suppression String]
else str([VALUE])
end

Note: because the [Suppression String] is a string, you have to convert your numbers to strings. There’s a bit more you’ll need to do if you need the numbers rounded, converted to percentages, etc. but this is the basic logic. If you need a numeric suppression, such as for axes, then you can write:

if [VALUE]<[Suppression Max] then NULL
else [VALUE]
end

There! Easy suppression! End of blog, right?

Nope. Now your end user needs the grand totals showing. In the bar graph example above, since only one number is suppressed, if you add a grand total in then your end users can subtract all the non-suppressed values from the grand total to figure out the suppressed value. That’s where complementary suppression comes in. Complementary suppression is when you need to suppress any number under a given value, and then the next lowest value after that, so that you’ll always have a minimum of two values suppressed and people cannot math out what the suppressed values are.

Now, at this point, it would be easiest to do the suppression on the database side and just bring the numbers with the suppression into Tableau. If you have that option, TAKE IT. But sometimes you don’t. So let’s create some calcs that will make this crosstab:

Look like this:

In the above table, the requirements are “implement complementary suppression for any value less than 10, and if the two suppressed values add to less than 10, supress the next lowest value.” Taking a look at the first table, you can see that there are two values less than 10, and that is the 2 at row A and 7 at row L. However, since 7+2 is 9, which is less than 10, we have to suppress the next lowest value as well, which is 17 at row J.

Why go through all of this? Some places have very strict suppression requirements so this level of suppression is required.

Let’s play whack-a-number!

Step 0: Initial Setup of Data

In order to make sure we’re suppressing the lowest values, we’ll need a couple of variables setup before we start the whack-a-mole. We will set a few extra variables in this step because after this example we’re going to do one more, a full crosstab, where we’ll need some extra set-up.

Variable Count

This is your measure. In this example it’s just a simple sum(1).

Value Formatted

//FORMATTING THE STRING CONVERTION SUM OF VARIABLE COUNT TO SHOW IN NUMBER FORMAT BY ADDING ‘,’
//If there are additional data types their string conversion would happen here

REGEXP_REPLACE(STR(sum([Variable Count])),”(\d)(?=(\d{3})+$)”,”$0,”)

Because the final table will have to be strings, not numbers, to accommodate the suppression string, we need to make sure the final values are formatted as strings as well. I use this REGEXP_REPLACE formula when I’m changing number values to strings because it will add in thousands separator values, making the table formatted in a much more user-friendly way.

Rank by row

rank_dense(sum([Variable Count]),’asc’)

This ranks your values and will be calculated along table across. This way we have an order for which values in the row are highest/lowest.

Rank by column

rank_dense(sum([Variable Count]), ‘asc’)

This ranks your values and will be calculated along table down at the end when we start editing our nested table calcs. This way we have an order for which values in the column are highest/lowest.

Size row

size()

The size function is awesome and counts the number of rows/columns in a view, depending on if it’s set up as table across or table down. When you turn on Grand Totals, the Grand Total column/row will have a size() of 1. For this, we need two size calculations so that one can be table across and one can be table down, so we can ensure our totals aren’t being suppressed.

Size column

size()

Suppression Example 2: Suppress a one row or column table with a grand total

Step 1: Initial Suppression

First see if any suppression even needs to happen

1. Suppress Value Table Initial

//CHECK IF VARIABLE COUNT AT ITEM LEVEL IS LESS THAN SUPPRESSION VALUE
{include [Group by Item Row]:sum([Variable Count])}< [Suppression Max]

Step 2. Secondary Suppression

Now we need to make sure at least two values are suppressed, and that those two values, when added together, are greater than the suppression cutoff (in this case, they need to add to be more than 10).

2. Suppress Value Table Secondary

if [Size column]=1 then FALSE
//Nothing in this column is being suppressed
elseif not total(sum(if [1. Suppress Value Table Initial] then 1 else 0 end))>0 then FALSE
// Is the sum of the suppressed values greater then the suppression max
elseif total(sum(if [1. Suppress Value Table Initial] then [Variable Count] else 0 end))
>[Suppression Max]
then
// We don’t need secondary suppression
if max([1. Suppress Value Table Initial]) then TRUE
else FALSE
END
// Suppress the inital suppression
elseif max([1. Suppress Value Table Initial]) then TRUE
//Find the lowest value that is not suppressed and use it for complementary suppression
//Higher ranked values will be false and therefore not suppressed
elseif [Rank by column]
-total(sum(if [1. Suppress Value Table Initial]
then 1 else 0 end))=1 then TRUE
else FALSE
end

Step 3: Put it all together

Now that we’ve flagged the initial and secondary suppression, put them all together. Because this table has a grand total, we want to ensure that we don’t over or under suppress that value, so we use the Size variable to account for that.

3. Suppression Final Table

if [Size column]=1
//it’s a total row but still under the suppression max
then if sum([Variable Count])<[Suppression Max] then [Suppression String]
else [Value Formatted]
end
elseif [2. Suppress Value Table Secondary] then [Suppression String]
else [Value Formatted]
end

You will need to ensure the table calc is calculating correctly. To do that, bring 3. Suppression Final Table to the Text marks card, right-click, and go to “Edit Table Calculation.” Make sure they are all set up like the following:

And voila! You have suppression!

Adding in the percentages takes a bit more work. I added those calculations in the Tableau workbook, you can always go check those out.

Example 3: Suppressing a Crosstab

All right, that wasn’t too bad, was it? For my final suppression example, I want to suppress a crosstab. This will make the table seem like a cake walk. Here’s the crosstab:

A crosstab table with all numbers showing.

We want it to look like this:

A crosstab table with complementary suppression.

In the above tables, the requirements are “implement complementary suppression for any value less than 10.” Taking a look at the first table, you can see that there is only one value less than 10, and that is the 2 at row A, Group 1. So why are 6 values now suppressed? Let’s walk through it:

A crosstab table explaining why each value was suppressed.
  1. The 2 is suppressed because it’s less than 10
  2. The orange 15 and orange 17 are suppressed because of the complementary suppression. If those weren’t suppressed then you could add together all Groups 2, 3, and 4, subtract them from the grand total, and figure out that the suppressed value is 2.
  3. When the secondary suppression is applied, the 15 is the only value in Group 2 suppressed, and the 17 is the only value in row J suppressed. So now someone could add together all of row J, subtract from the grand total, figure out that is 17, and then do that again to Group1 and figure out that the value is 2. So then the tertiary suppression comes in. The red 20 is suppressed to be complementary to Group2 (orange 15) and the the red 11 is suppressed to be complementary to row J (orange 17).
  4. Doing that caused the red 11 to be the only value in Group 3 suppressed, so now the green 16 in row L is suppressed. This is Quad suppression. In this example we stop here because the hope is that by going through the quaternary level, even if there is a single value (like the green 16), there’s enough other values suppressed that people can’t math back to the yellow 2.

Step 1: Identify the initial values that need to be suppressed.

Create two calculations to do the initial suppression. Because we’re going both down and across with the complementary suppression we have to create calculations that go in both directions.

This looks at the dimension that is on the Rows and flags anything that is less than the parameter you set up to be the max suppression value.

1a. Suppress Value Row Initial

Do the same thing for Columns.

1b. Suppress Value Column Initial]

The outcome of these formulas are true/false, is it above/below the suppression value.

Step 2: Start looking at the secondary suppression

You’ll create two more calculations to look at the secondary suppression. This is just looking to see if there is any primary suppression in rows or columns, and then doing that first look to suppress a second value so that specific row or column cannot be added together (the orange values from this table).

A crosstab table explaining why each value was suppressed.

Your calculations are:

2a. Suppress Value Row Secondary

//First, check if nothing in this column is being suppressed and if nothing is being suppressed then default to false
if(if not total(sum(if [1a. Suppress Value Row Initial] then 1 else 0 end))>0 then FALSE

// Is the sum of the suppressed values greater then the suppression max
elseif total(sum(if [1a. Suppress Value Row Initial] then [Variable Count] else 0 end))
>[Suppression Max]
then

// We don’t need secondary suppression
if max([1a. Suppress Value Row Initial]) then TRUE
else FALSE
END

// Suppress the initial suppression
elseif max([1a. Suppress Value Row Initial]) then TRUE

//Find the lowest value that is not suppressed and use it for complementary suppression
//Higher ranked values will be false and therefore not suppressed
else [Rank by row]
-total(sum(if [1a. Suppress Value Row Initial]
then 1 else 0 end))=1
end) then [Suppression String]
else ([Value Formatted])
END

2b. Suppress Value Column Secondary

//Nothing in this column is being suppressed
if(if not total(sum(if [1b. Suppress Value Column Initial] then 1 else 0 end))>0 then FALSE
// Is the sum of the suppressed values greater then the suppression max
elseif total(sum(if [1b. Suppress Value Column Initial] then [Variable Count] else 0 end))
>[Suppression Max]
then
// We don’t need secondary suppression
if max([1b. Suppress Value Column Initial]) then TRUE
else FALSE
END
// Suppress the initial suppression
elseif max([1b. Suppress Value Column Initial]) then TRUE
//Find the lowest value that is not suppressed and use it for complementary suppression
//Higher ranked values will be false and therefore not suppressed
else [Rank by column]
-total(sum(if [1b. Suppress Value Column Initial]
then 1 else 0 end))=1
end) then [Suppression String]
else ([Value Formatted])
END

Step 3. Let the whacking begin!

With those calculations, we have the yellow and orange suppressed. But now people could use row J to calculate what Group 1, row J value is. So we need more suppression. The 11 will be suppressed because row J has one suppressed value (17) and 20 will be suppressed because row Group 2 has only one value suppressed.

A crosstab table explaining why each value was suppressed.

3a. Secondary Suppression on Columns

//Add secondary suppression on columns based on the rows
if window_sum(if [2a. Suppress Value Row Secondary]=[Suppression String] then 1 else 0 end)=1 and [Rank by column]<=2
then [Suppression String]
else [Value Formatted]
END

3b. Secondary Suppression on Row

//Add secondary suppression on columns based on the rows
if window_sum(if [2b. Suppress Value Column Secondary]=[Suppression String] then 1 else 0 end)=1
and [Rank by row]<=2
then [Suppression String]
else [Value Formatted]
END

Step 4: Combine all the suppression to this point

Now we have a bunch of suppression calculations, so let’s implement them in the table.

4. Tertiary suppression

//Now that secondary suppression has been done on rows, then on columns, we combine those so they are suppressed
if [2a. Suppress Value Row Secondary]=[Suppression String] then [Suppression String]
elseif [2b. Suppress Value Column Secondary]=[Suppression String] then [Suppression String]
elseif [3a. Secondary Suppression on Columns]=[Suppression String] then [Suppression String]
ELSEIF [3b. Secondary Suppression on Row]=[Suppression String] then [Suppression String]
else [Value Formatted]
END

Step 5: Quad Suppression

This last suppression will take care of the green 16 in the spreadsheet above. Technically, that will only have one value suppressed but honestly, we could play whack-a-mole until every value is hidden. So in this example we stopped at quad, because the main goal of all of this is to hide the initial yellow value of 2. With 4 levels of suppression, users should not be able to figure out that value, even if they can figure out this 16.

5a. Quad suppression in row

//Finally, since after adding the column secondary suppression could result in a row having just 1 value suppressed again,
//Go back through the rows, check for just one suppression, and re-suppress.
if WINDOW_SUM(if [4. Tertiary suppression]=[Suppression String] then 1 else 0 end)=1 and [Rank by row]<=2 then [Suppression String]
else ifnull([4. Tertiary suppression],’0')
END

5b. Quad suppression in column

//Finally, since after adding the column secondary suppression could result in a row having just 1 value suppressed again,
//Go back through the rows, check for just one suppression, and re-suppress.
if WINDOW_SUM(if [5a. Quad suppression in row]=[Suppression String] then 1 else 0 end)=1
and [Rank by column]<=2 then [Suppression String]
else ifnull([5a. Quad suppression in row],’0')
END

Step 6: Remove some over-suppressed values

With the calculations above, it is possible to have your values over-suppressed. So let’s do some final calcs to help make sure totals and such aren’t actually being suppressed. First, let’s calculate some totals, mins, and maxes along rows and columns.

6a. total cell values suppressed in row

window_sum(
if [5b. Quad suppression in column]=[Suppression String]
then sum({include [Group by Item Row]:sum([Variable Count])}) end)

6b. min cell values suppressed in row

window_min(
if [5b. Quad suppression in column]=[Suppression String]
then min({include [Group by Item Row]:sum([Variable Count])}) end)

6c. max cell values suppressed in row

window_max(
if [5b. Quad suppression in column]=[Suppression String]
then max({include [Group by Item Row]:sum([Variable Count])}) end)

6d. total values suppressed in column

window_sum(
if [5b. Quad suppression in column]=[Suppression String]
then sum([Variable Count]) end)

6e. min suppression of columns in row

window_min([6d. total values suppressed in column])

6f. min values suppressed in column

window_min(
if [5b. Quad suppression in column]=[Suppression String]
then min([Variable Count]) end)

Step 7: Put it all together! Part 1.

Because the total can get suppressed, which can cascade into over suppression, the final variable needs to have checks for that. So here’s the first step of putting it all together.

7a. suppression with total checks row

if [Size row]=1
and [6a. total cell values suppressed in row]>[6b. min cell values suppressed in row]
and sum([Variable Count])>[Suppression Max]
then ‘Show row total’ //[Value Formatted]
elseif [6c. max cell values suppressed in row]=sum([Variable Count]) then ‘hide row’
else [5b. Quad suppression in column]
end

7b. suppression with total checks column

if [Size column]=1
and [6e. min suppression of columns in row]<[Suppression Max]
and [Rank by row]<=2
then [Suppression String]//’hide row total’
elseif [Size column]=1
and [6d. total values suppressed in column]>[6f. min values suppressed in column]
and sum([Variable Count])>[Suppression Max]
then [Value Formatted]
elseif [Size column]=1 then [5b. Quad suppression in column]
elseif [7a. suppression with total checks row]=’Show row total’ then [Value Formatted]
else [5b. Quad suppression in column]
end

8a. min suppressed in column

window_min(if [7b. suppression with total checks column]=[Suppression String]
then sum([Variable Count]) END)

9. final suppression (yes, that does say final!!)

if [Size row]=1
and [8a. min suppressed in column]<[Suppression Max]
and [Rank by column]<=2
then [Suppression String]//’hide row total’
else [7b. suppression with total checks column]
end

Step 7: Put it all together! Part 2: setting up the table.

You did it! You have all your calculations pulled together! Now let’s assemble the actual crosstab.

Put Group by Item Column on columns and Group by Item Row on rows. Add 9. final suppression onto the Text marks card. This is a table calculation, so we need to set up all the nested calculations underneath. I have screenshots of each one below.

Then you’re done! Congrats on finally ending your game of whack-a-mole. Now that you’ve seen the steps, do yourself a favor and try to get this all implemented at the datasource level so you don’t have to set this all up.

Nested Calculation Order

--

--

Nicole Klassen

A data viz lover, passionate about always learning and helping others.