Graph for relationship between two ordinal variables

What is an appropriate graph to illustrate the relationship between two ordinal variables?

A few options I can think of:

1. Scatter plot with added random jitter to stop points hiding each other. Apparently a standard graphic - Minitab calls this an "individual values plot". In my opinion it may be misleading as it visually encourages a kind of linear interpolation between ordinal levels, as if the data were from an interval scale.
2. Scatter plot adapted so that size (area) of point represents frequency of that combination of levels, rather than drawing one point for each sampling unit. I have occasionally seen such plots in practice. They can be hard to read, but the points lie on a regularly-spaced lattice which somewhat overcomes the criticism of the jittered scatter plot that it visually "intervalises" the data.
3. Particularly if one of the variables is treated as dependent, a box plot grouped by the levels of the independent variable. Likely to look terrible if the number of levels of the dependent variable is not sufficiently high (very "flat" with missing whiskers or even worse collapsed quartiles which makes visual identification of median impossible), but at least draws attention to median and quartiles which are relevant descriptive statistics for an ordinal variable.
4. Table of values or blank grid of cells with heat map to indicate frequency. Visually different but conceptually similar to the scatter plot with point area showing frequency.

Are there other ideas, or thoughts on which plots are preferable? Are there any fields of research in which certain ordinal-vs-ordinal plots are regarded as standard? (I seem to recall frequency heatmap being widespread in genomics but suspect that is more often for nominal-vs-nominal.) Suggestions for a good standard reference would also be very welcome, I am guessing something from Agresti.

If anyone wants to illustrate with a plot, R code for bogus sample data follows.

"How important is exercise to you?" 1 = not at all important, 2 = somewhat unimportant, 3 = neither important nor unimportant, 4 = somewhat important, 5 = very important.

"How regularly do you take a run of 10 minutes or longer?" 1 = never, 2 = less than once per fortnight, 3 = once every one or two weeks, 4 = two or three times per week, 5 = four or more times per week.

If it would be natural to treat "often" as a dependent variable and "importance" as an independent variable, if a plot distinguishes between the two.

importance <- rep(1:5, times = c(30, 42, 75, 93, 60))
often <- c(rep(1:5, times = c(15, 07, 04, 03, 01)), #n=30, importance 1
rep(1:5, times = c(10, 14, 12, 03, 03)), #n=42, importance 2
rep(1:5, times = c(12, 23, 20, 13, 07)), #n=75, importance 3
rep(1:5, times = c(16, 14, 20, 30, 13)), #n=93, importance 4
rep(1:5, times = c(12, 06, 11, 17, 14))) #n=60, importance 5
running.df <- data.frame(importance, often)
cor.test(often, importance, method = "kendall") #positive concordance
plot(running.df) #currently useless


A related question for continuous variables I found helpful, maybe a useful starting point: What are alternatives to scatterplots when studying the relationship between two numeric variables?

A spineplot (mosaic plot) works well for the example data here, but can be difficult to read or interpret if some combinations of categories are rare or don't exist. Naturally it's reasonable, and expected, that a low frequency is represented by a small tile, and zero by no tile at all, but the psychological difficulty can remain. It's also natural that people fond of spineplots choose examples which work well for their papers or presentations, but I've often produced examples that were too messy to use in public. Conversely, a spineplot does use the available space well.

Some implementations presuppose interactive graphics, so that the user can interrogate each tile to learn more about it.

An alternative which can also work quite well is a two-way bar chart (many other names exist).

See for example tabplot within http://www.surveydesign.com.au/tipsusergraphs.html

For these data, one possible plot (produced using tabplot in Stata, but should be easy in any decent software) is

The format means it is easy to relate individual bars to row and column identifiers and that you can annotate with frequencies, proportions or percents (don't do that if you think the result is too busy, naturally).

Some possibilities:

1. If one variable can be thought of a response to another as predictor, then it is worth thinking of plotting it on the vertical axis as usual. Here I think of "importance" as measuring an attitude, the question then being whether it affects behaviour ("often"). The causal issue is often more complicated even for these imaginary data, but the point remains.

2. Suggestion #1 is always to be trumped if the reverse works better, meaning, is easier to think about and interpret.

3. Percent or probability breakdowns often make sense. A plot of raw frequencies can be useful too. (Naturally, this plot lacks the virtue of mosaic plots of showing both kinds of information at once.)

4. You can of course try the (much more common) alternatives of grouped bar charts or stacked bar charts (or the still fairly uncommon grouped dot charts in the sense of W.S. Cleveland). In this case, I don't think they work as well, but sometimes they work better.

5. Some might want to colour different response categories differently. I've no objection, and if you want that you wouldn't take objections seriously any way.

The strategy of hybridising graph and table can be useful more generally, or indeed not what you want at all. An often repeated argument is that the separation of Figures and Tables was just a side-effect of the invention of printing and the division of labour it produced; it's once more unnecessary, just as it was to manuscript writers putting illustrations exactly how and where they liked.

• Thanks for adding the graphic. This raises the issue of how graphics and textual data combine - I know some people don't like putting numbers on the top of bars (because it makes the bars appear taller than they really are; I don't have a citation to hand for this but I think it's a well-known opinion). – Silverfish Feb 13 '15 at 18:17
• On the other hand, fixing the position of the numbers seems to create one of two problems: either the numbers can end up superimposed on the bars, which obscures them, or fixing the numbers above the bars can "disconnect" them from the lower bars in particular. Is there a good discussion of these issues somewhere? – Silverfish Feb 13 '15 at 18:18
• I don't think you need a reference; it's a common attitude. I see other variants: (1) display-specific suggestions that the display is just too busy, untidy, etc. (2) an appeal to the notion that the numeric text is redundant because the same information is implicit (or according to some explicit) in the graph any way (3) a "boys wear blue and girls wear pink" attitude that Figures are figures and Tables are tables, and ne'er the twain shall meet. (3) strikes me as pure prejudice; (2) is correct in principle, but nevertheless numbers can help;(1) has to be thought through example by example. – Nick Cox Feb 13 '15 at 18:24
• I don't know discussions of the specific trade-offs. Leaving bars uncoloured so that numbers can be put inside them is often a good idea. Sometimes bars can be too small for this to be done always. – Nick Cox Feb 13 '15 at 18:26

Here is a quick attempt at a heat map, I have used black cell borders to break up the cells, but perhaps the tiles should be separated more as in Glen_b's answer.

library(ggplot2)
runningcounts.df <- as.data.frame(table(importance, often))
ggplot(runningcounts.df, aes(importance, often)) +
geom_tile(aes(fill = Freq), colour = "black") +
scale_fill_gradient(low = "white", high = "steelblue")


Here is a fluctuation plot based on an earlier comment by Andy W. As he describes them "they are basically just binned scatterplots for categorical data, and the size of a point is mapped to the number of observations that fall within that bin." For a reference see

Wickham, Hadley and Heike Hofmann. 2011. Product plots. IEEE Transactions on Visualization and Computer Graphics (Proc. Infovis 11). Pre-print PDF

theme_nogrid <- function (base_size = 12, base_family = "") {
theme_bw(base_size = base_size, base_family = base_family) %+replace%
theme(panel.grid = element_blank())
}

ggplot(runningcounts.df, aes(importance, often)) +
geom_point(aes(size = Freq, color = Freq, stat = "identity", position = "identity"), shape = 15) +
scale_size_continuous(range = c(3,15)) +
scale_color_gradient(low = "white", high = "black") +
theme_nogrid()

• "perhaps the tiles should be separated more as in Glen_b's answer" -- I'm not sure it's necessary in this case, there's much less temptation to see the categories as continuous here. – Glen_b Apr 18 '13 at 0:18

Here's an example of what a spineplot of the data would look like. I did this in Stata pretty quickly, but there's an R implementation. I think in R it should be just:

spineplot(factor(often)~factor(importance))

The spineplot actually seems to be the default if you give R categorical variables:

plot(factor(often)~factor(importance))

The fractional breakdown of the categories of often is shown for each category of importance. Stacked bars are drawn with vertical dimension showing fraction of often given the importance category. The horizontal dimension shows the fraction in each importance category. Thus the areas of tiles formed represent the frequencies, or more generally totals, for each cross-combination of importance and often.

• I changed it around. – Dimitriy V. Masterov Apr 17 '13 at 3:16
• Quoting Nick Cox (the author of Stata's spineplot): The restriction to two variables is more apparent than real. Composite variables may be created by cross-combination of two or more categorical variables.... A response variable is usually better shown on the y axis. If one variable is binary, it is often better to plot that on the y axis. Naturally, there can be some tension between these suggestions. – Dimitriy V. Masterov Apr 17 '13 at 3:57
• I agree with the above. But Stata's default colo[u]r scheme is fairly lousy for ordinal variables. Several good alternatives are different shades of red and/or blue, or just gr{a|e}yscale choices. – Nick Cox Apr 17 '13 at 10:31
• @Dimitriy I find it very strange to use an arbitrary mix of colours in the same situation! I don't imply or infer anything by or from the exact colours, however quantified. But the point is only that a graded scale is well matched by a graded sequence of colours. There is some arbitrariness in the colouring of heat maps too, and indeed in many kinds of thematic cartography. – Nick Cox Apr 17 '13 at 17:29
• I don't see the problem with a graded colour scheme so long as colours are distinct. Why would anyone be tempted to interpolate? I can't see a logic to arbitrary colours. Rainbow sequences make sense in physics, but not in terms of how people perceive colours (e.g. yellow and red are too different). I have evidence in terms of talking many students through choices, and I'd say 80% sincerely say "That's much better" when they see a subtle graded sequence over rainbows or fruit salad. Blue through pale blue through pale red to red works well. Make sure you try this out on women as well as men. – Nick Cox Apr 18 '13 at 8:59

The way I've done this is a bit of a fudge, but it could be fixed up easily enough.

This is a modified version of the jittering approach.

Removing the axes reduces the temptation to interpret the scale as continuous; drawing boxes around the jittered combinations emphasizes there's something like a "scale break" - that the intervals aren't necessarily equal

Ideally, the 1..5 labels should be replaced with the category names, but I'll leave that for the imagination for now; I think it conveys the sense of it.

 plot(jitter(often)~jitter(importance),data=running.df,bty="n",
ylim=c(0.5,5.5),xlim=c(0.5,5.5),cex=0.5,pty="s",xaxt="n",yaxt="n")
axis(1,tick=TRUE,col=0)
axis(2,tick=TRUE,col=0)
rect(rep(seq(0.75,4.75,1),5),rep(seq(0.75,4.75,1),each=5),
rep(seq(1.25,5.25,1),5),rep(seq(1.25,5.25,1),each=5),
border=8)


Possible refinements:

i) making the breaks smaller (I prefer larger breaks than this, personally), and

ii) attempting to use a quasirandom sequence to reduce the incidence of apparent pattern within the boxes. While my attempt helped somewhat, you can see that in the cells with smaller numbers of points there are still subsequences with a more or less correlated look (e.g. the box in the top row, 2nd column). To avoid that, the quasi-random sequence might have to be initialized for each sub-box. (An alternative might be Latin Hypercube sampling.) Once that was sorted out, this could be inserted into a function that works exactly like jitter.

library("fOptions")

hjit <- runif.halton(dim(running.df)[1],2)
xjit <- (hjit[,1]-.5)*0.8
yjit <- (hjit[,2]-.5)*0.8

plot(I(often+yjit)~I(importance+xjit),data=running.df,bty="n",
ylim=c(0.5,5.5),xlim=c(0.5,5.5),cex=0.5,pty="s",xaxt="n",yaxt="n")
axis(1,tick=TRUE,col=0)
axis(2,tick=TRUE,col=0)
rect(rep(seq(0.55,4.55,1),5),rep(seq(0.55,4.55,1),each=5),
rep(seq(1.45,5.45,1),5),rep(seq(1.45,5.45,1),each=5),
border=8)

• I like this, for me the separation really emphasises the ordinal nature of the data! Unfortunately the human eye is drawn naturally to apparent patterns in the jittering e.g. the "upwards trends" in panels (4,5) and (5,3). On the plus side "counting the points" feels much more natural to me than judging frequency by dot size. Are there variants where points are spaced evenly, or clumped in regular patterns at the centers, to avoid distracting "jitter trends"? – Silverfish Apr 17 '13 at 1:57
• @Silverfish, a similar concept in geography are dot-density maps. Geographers have found some evidence that regular patterns or patterns that fill up a certain amount of whitespace (so are spaced farther apart then random) tend to produce more accurate perceptions among observers. – Andy W Apr 17 '13 at 2:11
• IMO this is a fine idea, but the spacing between the panels is so large in this example it makes visualizing any trend very difficult. The cure is worse than the disease (but it should be pretty easy to make the panels much closer together). – Andy W Apr 17 '13 at 2:15
• @silverfish quasi-random jittering would be a possible solution to that. Your concern is one I had myself. – Glen_b Apr 17 '13 at 2:20
• Very nice! IMO this is a better option than the spineplot in this instance (spine or mosaic plots are better to assess conditional distributions for any category pair - this jittered dot plot is easier to assess trends - taking advantage of the ordinal nature of the data and assuming some type of monotonic relationship). – Andy W Apr 17 '13 at 12:15

Using the R package riverplot:

  data$importance <- factor(data$importance,
labels = c("not at all important",
"somewhat unimportant",
"neither important nor unimportant",
"somewhat important",
"very important"))
data$often <- factor(data$often,
labels = c("never",
"less than once per fortnight",
"once every one or two weeks",
"two or three times per week",
"four or more times per week"))

makeRivPlot <- function(data, var1, var2, ...) {

require(plyr)
require(riverplot)
require(RColorBrewer)

names1 <- levels(data[, var1])
names2 <- levels(data[, var2])

var1 <- as.numeric(data[, var1])
var2 <- as.numeric(data[, var2])

edges <- data.frame(var1, var2 + max(var1, na.rm = T))
edges <- count(edges)

colnames(edges) <- c("N1", "N2", "Value")

nodes <- data.frame(ID     = c(1:(max(var1, na.rm = T) +
max(var2, na.rm = T))),
x      = c(rep(1, times = max(var1, na.rm = T)),
rep(2, times = max(var2, na.rm = T))),
labels = c(names1, names2) ,
col    = c(brewer.pal(max(var1, na.rm = T), "Set1"),
brewer.pal(max(var2, na.rm = T), "Set1")),
stringsAsFactors = FALSE)

nodes$col <- paste(nodes$col, 95, sep = "")

return(makeRiver(nodes, edges))

}

a <- makeRivPlot(data, "importance", "often")

riverplot(a, srt = 45)


• (+1) I like the idea of using parallel coordinates for this! I think it would be easier to trace the paths through the diagram, and see how the "often" answers are decomposed, if the colours flowed from left to right (a scheme that would effectively be displaying "often" as the dependent variable and "importance" as the explanatory variable). On some interactive implementations of such plots you can click an axis to colour by that variable, which is useful. – Silverfish Jan 30 '15 at 10:42
• For comparison, Robert Kosara's "parallel sets" visualization, which is designed for categorical data, has the colours flowing through the diagram. – Silverfish Jan 30 '15 at 10:44

A different idea that I didn't think of originally was a sieve plot.

Size of each tile is proportional to expected frequency; the little squares inside the rectangles represent actual frequencies. Hence greater density of the squares indicates higher than expected frequency (and is shaded blue); lower density of squares (red) is for lower than expected frequency.

I think I'd prefer it if the color represented the size, not just sign, of the residual. This is particularly true for edge cases where expected and observed frequencies are similar and the residual is close to zero; a dichotomous red/blue scheme seems to overemphasise small deviations.

Implementation in R:

library(vcd)
runningcounts.df <- as.data.frame(table(importance, often))
sieve(Freq ~ often + importance, data=runningcounts.df, shade= TRUE)

• Regarding your preference that the colour represent size as well as sign, one possibility is to make the colours more grey when the difference from expected is relatively small. – Glen_b Feb 5 '15 at 0:27

A faceted bar chart in R. It shows the distribution of "often" at each level of "importance" very clearly. But it wouldn't have worked so well if the maximum count had varied more between levels of "importance"; it's easy enough to set scales="free_y" in ggplot (see here) to avoid lots of empty space, but the shape of the distribution would be hard to discern at low-frequency levels of "importance" since the bars would be so small. Perhaps in those situations it is better to use relative frequency (conditional probability) on the vertical axis instead.

It isn't so "clean" as the tabplot in Stata that Nick Cox linked to, but conveys similar information.

R code:

library(ggplot)
running2.df <- data.frame(often = factor(often, labels = c("never", "less than once per fortnight", "once every one or two weeks", "two or three times per week", "four or more times per week")), importance = factor(importance, labels = c("not at all important", "somewhat unimportant", "neither important nor unimportant", "somewhat important", "very important")))
ggplot(running2.df, aes(often)) + geom_bar() +
facet_wrap(~ importance, ncol = 1) +
theme(axis.text.x=element_text(angle = -45, hjust = 0)) +
theme(axis.title.x = element_blank())
`