# Which is the best visualization for contingency tables?

Which is best plot, from statistical standpoint, to show a contingency table, which is typically being analyzed by chi-square test? Is it a dodged barplot, stacked barplot, heatmap, contour plot, jitterred scatterplot, multiple lines plot or something else? Should one show absolute values or percentages?

Edit: Or as @forecaster suggests in the comments, the table of numbers is itself a simple plot and should be sufficient.

• Sometimes data table is the best visualization method vs. plots. Contingency tables is a classic example of that. Commented Apr 22, 2015 at 12:52
• Important point, though I do not agree that it is always the best option.
– rnso
Commented Apr 22, 2015 at 12:55
• That is why I said "sometimes". I would recommend, Stephen Few's Show me the numbers which has a whole section dedicated to tables. Commented Apr 22, 2015 at 12:58
• well, best method depends on what you want to show, how big is the table, without some specifics this is to broad! Commented Apr 22, 2015 at 14:25
• Most of stats.stackexchange.com/questions/56322/… seems pertinent here. Commented Apr 22, 2015 at 17:52

There isn't going to be a one-size-fits-all solution here. If you have a very simple table (e.g., $2\times 2$), simply presenting the table is probably best. If you want an actual figure, mosaic plots (as @xan suggests) are probably a nice place to start. There are some other options that are analogous to mosaic plots, including sieve plots, association plots, and dynamic pressure plots (see my question here: Alternative to sieve / mosaic plots for contingency tables); Michael Friendly's book, Visualizing Categorical Data, would be a good (SAS-based) resource for this topic and the vcd package is a good resource for implementing those ideas in R.

As tables have larger numbers of rows and columns, however, these become harder to use, in my opinion. A different type of visualization option is to perform / plot a correspondence analysis. A correspondence analysis is analogous to running a principal components analysis on both the rows and the columns of the contingency table. Then both are plotted together with a biplot. Here is an R based example using the data from @xan's answer:

library(ca)
tab = as.table(rbind(c(28, 4,  0, 56),
c(38, 5,  9, 10),
c( 6, 6, 14, 13) ))
names(dimnames(tab)) = c("activity", "period")
rownames(tab)        = c("feed", "social", "travel")
colnames(tab)        = c("morning", "noon", "afternoon", "evening")
tab
#         period
# activity morning noon afternoon evening
#   feed        28    4         0      56
#   social      38    5         9      10
#   travel       6    6        14      13
plot(ca(tab))


To interpret this plot, the closer two points of the same type are, the more similar those two row / column profiles are. And the closer two points of different types are, the more of their probability mass is in the cell representing their intersection.

In R there is the ca package; this vignette (pdf) may be helpful as well.

• Very useful. Apparently it fails with small values, eg: tt = with(mtcars, table(factor(gear), factor(vs))); plot(ca(tt)); Error in x[, dim] : subscript out of bounds
– rnso
Commented Apr 23, 2015 at 5:54
• That's because one of the factors (i.e., factor(vs)) has only two levels; you need at least three. Try ttt = with(mtcars, table(factor(gear), factor(cyl))); plot(ca(ttt)). Commented Apr 24, 2015 at 3:54
• Very good display of relations between different factors.
– rnso
Commented Apr 24, 2015 at 4:00
• Or you can show the table after reordering rows and columns in the order of correspondence analysis scores. Commented Jun 11, 2016 at 14:36
• Interesting idea, @kjetilbhalvorsen. I'm not sure how to get that from the ca object, so I coded it from scratch. Unless I made a mistake, you reorder the rows c(1,3,2) & the columns c(4,1,3,2). Having done that, I'm not sure what I'm supposed to see here. What do you have in mind? Commented Jun 11, 2016 at 16:15

Different visuals will be better at highlighting different features, but Mosaic plots work well for a general view (checking to see if anything stands out). Maybe that's what you meant by dodged bar plot. Like most options, they're not symmetric in that they represent relative frequencies better in one dimension than the other. A nice feature is that the marginal frequencies are also represented.

• This is good. Both numbers and proportions are represented. Numbers can also be put on plot. If order of x-axis item are arranged, it will look even better. By dodged I meant common barplot with categories side by side, rather than stacked up on each other (see position='dodge' on this page: r-bloggers.com/using-r-barplot-with-ggplot2).
– rnso
Commented Apr 23, 2015 at 0:34
• +1 This design often works very well indeed for relatively simple data; conversely I've found that it also rapidly becomes hard to think about for more complicated data, despite much emphasis in the literature on its extensibility to contingency tables of arbitrary dimension. Still, no design works well either in such circumstances. A small point with this example is that you appear to have accepted the default alphabetical order "afternoon" ... "noon" offered by your program, whereas preserving the time order seems a more natural choice. Commented Jun 23, 2015 at 16:43

I agree that the "best" plot doesn't exist independent of dataset, readership and purpose. For two measured variables, scatter plots are arguably the design that leaves all others in its wake, except for specific purposes, but no such market leader is evident for categorical data.

My aim here is just to mention a simple method, often re-discovered or re-invented, but nevertheless also often overlooked even in monographs or textbooks covering statistical graphics.

Example first, covering the same data as posted by xan:

If a name is wanted, as it often is, this is a twoway barchart (in this case). I will not catalogue other terms here, except that multiple barchart is one common alternative with similar flavour. (My small objection to "multiple barchart" is that "multiple" does not rule out the very common stacked or side-by-side bar charts, whereas "twoway" to me more clearly implies a row and column layout, although in turn it may take examples to make that clear.)

Pluses and minuses for this kind of plot are also simple, but I will spell some out. As I am fond of this design (which goes back at least to the 1930s), others may want to add sharper criticisms.

+1. The idea is easily understood, even by non-technical groups. Bar heights or bar lengths encode frequencies in this example. In other examples, they could encode percents calculated any way you like, residuals, etc.

+2. The row-and-column structure matches that of a table. You can add numerical values too. Very small amounts and even implicit zeros are clearly evident, which is not always the case with other designs (e.g. stacked bar charts, mosaic plots). Row and column labelling is usually more efficient than adding a key or legend, with the mental "back and forth" that that requires. Thus this design hybridizes graph and table ideas, which seemingly troubles some readers; conversely, I would argue that strong distinctions between Figures and Tables are just historical hang-overs, obsolete now that researchers can prepare their own documents and do not have to rely on designers, compositors and printers.

+3. Extensions to three-way and higher designs are easy in principle. Put two or more variables as composite variables on either or both axes, or give an array of such plots. Naturally, the more complicated the design, the more complicated the interpretation.

+4. The design clearly allows ordinal variables on either axis. Order can be expressed (e.g.) by appropriate shading as well as the order of categories on that axis. Category order on axes can be determined by their meaning, or better determined by frequencies; alphabetical order according to text labels may be a default, but should never be the only choice considered.

-1. By being general in design the plot can be less efficient in showing certain kinds of relationships. In particular, a mosaic plot can make departures from independence very clear. Conversely, when relationships between categorical variables are complicated or unclear, then typically no graph is good at showing more than that weak fact.

-2. In some ways the design is inefficient in use of space by leaving room for every cross-combination regardless of whether or how frequently it occurs. This is the vice of the same principle considered as a virtue. The particular design above spaces categories equally regardless of their frequency; sacrificing that often sacrifices readable marginal labels, which I value very highly. In this example, the text labels happen all to be very short, but that is far from typical.

Note: xan's data appear just to be invented, so I won't try an interpretation any more than is attempted in other answers. But some homespun wisdom deserves the last word here: the best design for you is one that best conveys to you and your readers the structure of some real data that you care about.

Other examples include

How can you visualize the relationship between 3 categorical variables?

Graph for relationship between two ordinal variables

EDIT The general idea, and a Stata implementation, are now written up in this paper.

• +1 for another viable option & some great discussion. Let me ask a question about 2-way barcharts / point to another possible downside: There is an unmarked, but clearly perceivable 'box' within which the bars may be plotted. As the bar approaches the top of the box, it reaches 100%. How is that value determined? (NB, contingency tables are always multinomial in some sense, w/ a known total.) Making the top of the box row sums or column sums will promote different perceptual inferences. (It does not appear that the table sum is used, as many bars would be too small to differentiate.) Commented Jun 23, 2015 at 17:08
• @gung Thanks. I can comment on my own Stata program, used here and in my examples elsewhere in CV. The program is tabplot from SSC. The height of the bars is necessarily some fraction of the height of the tallest or longest bar; the space available for that is determined by how many rows are being shown. The user can override the default gap size, but then risks bars touching or occluding each other. If bars can be negative as well as positive, things are not easier. I imagine that the same constraint bites with any other program. In short, bars not touching implies white space! Commented Jun 23, 2015 at 17:18
• Updating the comment above: for readers using Stata, the command tabplot is now documented through the Stata Journal (link added to the answer above in an EDIT). The paper is accessible to all and people not using Stata code should naturally fly straight past the code, even while they're wondering how to do it in their own favourite software. Commented May 28 at 17:53

To complement @gung's and @xan's answers, here's an example of mosaic and association plots using vcd in R.

> tab
period
activity morning noon afternoon evening
feed        28    4         0      56
social      38    5         9      10
travel       6    6        14      13


To obtain the plots:

require(vcd)


Both intuitively present departures from expected frequencies... The default is the model of mutual independence, but it can be changed (e.g. to joint independence if there is a clear response variable) via the expected argument.

One idea that is sometimes useful, especially for somewhat large tables, is to reorder rows/columns to make any structure clearer. One way to do the reordering is to use the sort order of the correspondence analysis row/column scores (on the first eigenvalue). I will show this by an example, in R:

data(hobbies, package="FactoMineR")

htable <- matrix(as.numeric(NA), nrow=18, ncol=7,
dimnames=list( colnames(hobbies)[1:18],
names(with(hobbies, table(Profession)))))

### Then filling the tables with values:

for(hob in rownames(htable)) {
htable[hob, ] <- table(hobbies[, hob],  hobbies[, "Profession"])[2,]
}

### This might be criticized,  because it gives more weight to
### people with more hobbies


Then we do the simple correspondence analysis, reorder, and some simplification of names to allow the table to fit on a page:

### Reordered table:

reorder_table <- function(tab) {
catable <-  MASS::corresp(tab)
RS <- catable$$rscore ; CS <- catable$$cscore
tab[order(RS), order(CS)]
}

htable_reordered <- reorder_table(htable)

colnames(htable_reordered) <- abbreviate(colnames(htable_reordered))

round(prop.table(htable_reordered, 1), 2)

Mngm Frmn Othr Tchn Empl Mnll Unsw
Exhibition      0.28 0.15 0.03 0.06 0.33 0.10 0.04
Show            0.27 0.13 0.03 0.07 0.37 0.09 0.04
Volunteering    0.24 0.15 0.04 0.06 0.35 0.11 0.05
Playing music   0.25 0.13 0.04 0.07 0.37 0.09 0.06
Travelling      0.26 0.14 0.03 0.07 0.33 0.11 0.06
Computer        0.23 0.13 0.03 0.08 0.35 0.12 0.06
Sport           0.23 0.14 0.03 0.07 0.34 0.12 0.06
Cinema          0.23 0.13 0.03 0.06 0.37 0.11 0.07
TV              0.21 0.13 0.04 0.06 0.34 0.14 0.08
Reading         0.20 0.12 0.03 0.05 0.39 0.13 0.08
Walking         0.18 0.13 0.03 0.06 0.37 0.15 0.09
Listening music 0.17 0.11 0.03 0.06 0.38 0.15 0.10
Gardening       0.18 0.12 0.03 0.06 0.34 0.18 0.09
Mechanic        0.18 0.13 0.03 0.08 0.29 0.21 0.09
Collecting      0.16 0.11 0.02 0.07 0.38 0.17 0.09
Cooking         0.15 0.10 0.03 0.05 0.42 0.14 0.10
Knitting        0.10 0.09 0.04 0.03 0.54 0.08 0.12
Fishing         0.13 0.12 0.02 0.08 0.21 0.28 0.14


To help the reading, a table of names with abbreviated names:

 "Unskilled worker" "Unsw"
"Manual labourer"  "Mnll"
"Technician"       "Tchn"
"Foreman"          "Frmn"
"Management"       "Mngm"
"Employee"         "Empl"
"Other"            "Othr"


The group "Other" seems strange, with uniformly lower interest in hobbies than all the others! And look at "Fishing", the groups with a high general interest in hobbies have a low interest, while it for "Manual labourer" and "Unskilled worker" scores the highest. Is this as easy to see in the original table?

Edit

But still the reordered table can be difficult to read. We try to make it better. First, express the table as percents and not proportions. Then we will color-code the numbers using package gt. The result below:

R code for this:

rtable <- round(100*prop.table(htable_reordered, 1), 0)

library(gt)

as.data.frame(rtable) |> gt(rownames_to_stub=TRUE) |>
data_color(palette="Blues", method="numeric", apply_to="text",
domain=c(-10,54)) |> gtsave(filename="./gthobbies.png")