Skip to contents

Introduction

Mosaic plots are a powerful visualization tool for categorical data, showing the relationships between variables through the sizes of tiles. However, a basic mosaic plot only shows the observed frequencies. To understand whether patterns in the data are statistically meaningful, we need to compare observed frequencies with what we would expect under some model of independence or association.

This is where extended mosaic plots come in. An extended mosaic plot fits a loglinear model to the contingency table and shades the tiles according to the Pearson residuals from that model:

rij=observedijexpectedijexpectedij r_{ij} = \frac{\text{observed}_{ij} - \text{expected}_{ij}}{\sqrt{\text{expected}_{ij}}}

Tiles shaded blue indicate observed frequencies higher than expected (positive association), while red tiles indicate frequencies lower than expected (negative association). The intensity of the color represents the magnitude of the residual.

This approach, pioneered by the vcd package, is now available in ggmosaic through the expected parameter in geom_mosaic().

Basic Example: Independence Model

The most common model is complete independence, where we assume no associations among the variables. Let’s explore this with the Titanic data:

data(titanic)
head(titanic)
#>   Class  Sex   Age Survived
#> 1   3rd Male Child       No
#> 2   3rd Male Child       No
#> 3   3rd Male Child       No
#> 4   3rd Male Child       No
#> 5   3rd Male Child       No
#> 6   3rd Male Child       No

Simple Mosaic Plot (No Model)

First, let’s create a basic mosaic plot showing Class by Survival:

print(ggplot(data = titanic,
             aes(x = product(Class, Survived), fill = Survived)) +
  geom_mosaic() +
  labs(title = "Titanic: Class by Survival",
       subtitle = "Basic mosaic plot") +
  theme_mosaic())

This shows the observed patterns, but we can’t immediately see which differences are statistically meaningful.

Extended Mosaic Plot with Residual Shading

Now let’s add an independence model to see deviations from expected frequencies:

print(ggplot(data = titanic, aes(x = product(Class, Survived))) +
  geom_mosaic(expected = "independence") +
  scale_fill_residual() +
  labs(title = "Titanic: Class by Survival",
       subtitle = "Independence model with residual shading") +
  theme_mosaic())

The blue and red shading immediately reveals the pattern: First class passengers had higher survival rates than expected (blue), while crew and third class had lower rates (red).

Model Specification

The expected parameter accepts three types of input:

1. Shortcut Strings

Three convenient shortcuts for common models:

  • "independence" - Complete independence (main effects only)
  • "saturated" - Saturated model (all interactions, no residuals)
  • "conditional" - Conditional independence given conditioning variables
# Independence model
p1 <- ggplot(data = titanic, aes(x = product(Class, Sex))) +
  geom_mosaic(expected = "independence") +
  scale_fill_residual() +
  labs(title = "Independence Model",
       subtitle = "~ Class + Sex") +
  theme_mosaic()

# Saturated model (no residuals - perfect fit)
p2 <- ggplot(data = titanic, aes(x = product(Class, Sex))) +
  geom_mosaic(expected = "saturated") +
  scale_fill_residual() +
  labs(title = "Saturated Model",
       subtitle = "~ Class * Sex") +
  theme_mosaic()

print(p1)

print(p2)

Note that the saturated model shows no shading because it perfectly fits the data (all residuals = 0).

2. Custom Formulas

You can specify custom loglinear models using R’s formula syntax:

# Model with Class + Sex main effects only (no interaction)
print(ggplot(data = titanic, aes(x = product(Class, Sex, Survived))) +
  geom_mosaic(expected = ~ Class + Sex) +
  scale_fill_residual() +
  labs(title = "Custom Model: Class + Sex",
       subtitle = "Testing for Survival associations given Class and Sex") +
  theme_mosaic())

This model asks: “Are there deviations from what we’d expect if Survival were independent of the Class × Sex combinations?”

3. Conditional Independence

When using conditioning variables with conds, the "conditional" shortcut is particularly useful:

# Conditional independence: Health and Marital Status given Sex
ggplot(data = happy,
       aes(x = product(health, marital), conds = sex)) +
  geom_mosaic(expected = "conditional") +
  scale_fill_residual() +
  labs(title = "Health × Marital | Sex") +
  theme_mosaic()

Interpreting Residuals

What Do Residuals Mean?

Pearson residuals follow approximately a standard normal distribution:

  • |r| < 2: Not significantly different from expected (white/light shading)
  • |r| > 2: Significantly different at α ≈ 0.05 (moderate blue/red)
  • |r| > 4: Highly significant (dark blue/red)

Three-Way Tables

Extended mosaic plots are particularly powerful for three-way and higher tables:

print(ggplot(data = titanic, aes(x = product(Class, Sex, Survived))) +
  geom_mosaic(expected = "independence") +
  scale_fill_residual() +
  labs(title = "Titanic: Complete Independence Model",
       subtitle = "Class, Sex, and Survival all independent") +
  theme_mosaic())

The residuals reveal complex patterns: - First class females had much higher survival (strong blue) - Crew males had lower survival (red) - The independence assumption is clearly violated

Labeling Cells with Values

To make residuals more interpretable, you can display the actual values in cells using geom_mosaic_text():

Display Observed Counts

print(ggplot(data = titanic, aes(x = product(Class, Sex))) +
  geom_mosaic(aes(fill = Survived)) +
  geom_mosaic_text(display_values = "observed",
                   format_digits = 0,
                   size = 3) +
  labs(title = "Observed Frequencies") +
  theme_mosaic())

Display Residuals

When using residual shading, it’s helpful to show the actual residual values:

print(ggplot(data = titanic, aes(x = product(Class, Sex))) +
  mosaic_settings(expected = "independence") +
  geom_mosaic() +
  scale_fill_residual() +
  geom_mosaic_text(display_values = "residual",
                   format_digits = 2,
                   colour = "black",
                   size = 3) +
  labs(title = "Residuals from Independence",
       subtitle = "Values show Pearson residuals") +
  theme_mosaic())

Display Expected Frequencies

You can also show what the model expects:

print(ggplot(data = titanic, aes(x = product(Class, Survived))) +
  mosaic_settings(expected = "independence") +
  geom_mosaic() +
  scale_fill_residual() +
  geom_mosaic_text(display_values = "expected",
                   format_digits = 1,
                   colour = "black",
                   size = 3.5) +
  labs(title = "Expected Frequencies Under Independence") +
  theme_mosaic())

The Four Display Options

The display_values parameter in geom_mosaic_text() accepts:

  1. "label" (default) - Factor level labels
  2. "observed" - Observed counts from the data
  3. "expected" - Expected values from the fitted model
  4. "residual" - Pearson residuals

Customizing the Color Scale

The scale_fill_residual() function provides a diverging color scale centered at zero. You can customize it:

print(ggplot(data = titanic, aes(x = product(Class, Sex))) +
  mosaic_settings(expected = "independence") +
  geom_mosaic() +
  scale_fill_residual(
    low = "firebrick",
    mid = "white",
    high = "steelblue",
    limits = c(-6, 6),  # Set symmetric limits
    name = "Pearson\nResidual"
  ) +
  geom_mosaic_text(display_values = "residual",
                   format_digits = 1,
                   size = 3) +
  labs(title = "Custom Color Scale") +
  theme_mosaic())

You can also use any ggplot2 diverging scale:

print(ggplot(data = titanic, aes(x = product(Class, Survived))) +
  geom_mosaic(expected = "independence") +
  scale_fill_gradient2(
    low = "purple",
    mid = "gray95",
    high = "orange",
    midpoint = 0,
    name = "Residual"
  ) +
  labs(title = "Custom ggplot2 Scale") +
  theme_mosaic())

Text Aesthetics

All standard ggplot2 text aesthetics work with geom_mosaic_text():

print(ggplot(data = titanic, aes(x = product(Class, Survived))) +
  mosaic_settings(expected = "independence") +
  geom_mosaic() +
  scale_fill_residual() +
  geom_mosaic_text(display_values = "residual",
                   format_digits = 2,
                   size = 4,           # Text size
                   colour = "white",   # Text color
                   fontface = "bold",  # Font weight
                   family = "serif") + # Font family
  labs(title = "Custom Text Aesthetics") +
  theme_mosaic())

Available text parameters: - size (default: 2.7) - colour/color - fontface: “plain”, “bold”, “italic”, “bold.italic” - family: Font family name - angle: Rotation angle in degrees - hjust, vjust: Justification (0-1) - lineheight: For multi-line text

Complete Example: Comparing Models

Let’s compare three different models for the same data:

# 1. Independence of all three variables
p1 <- ggplot(data = titanic, aes(x = product(Class, Sex, Survived))) +
  mosaic_settings(expected = "independence") +
  geom_mosaic() +
  scale_fill_residual(limits = c(-10, 10)) +
  geom_mosaic_text(display_values = "residual",
                   format_digits = 1,
                   size = 2.5) +
  labs(title = "Complete Independence",
       subtitle = "~ Class + Sex + Survived") +
  theme_mosaic()

# 2. Survival independent of Class and Sex jointly
p2 <- ggplot(data = titanic, aes(x = product(Class, Sex, Survived))) +
  mosaic_settings(expected = ~ Class + Sex) +
  geom_mosaic() +
  scale_fill_residual(limits = c(-10, 10)) +
  geom_mosaic_text(display_values = "residual",
                   format_digits = 1,
                   size = 2.5) +
  labs(title = "Survival Independent of Class × Sex",
       subtitle = "~ Class + Sex (no Survived interaction)") +
  theme_mosaic()

# 3. Class and Sex independent, both related to Survival
p3 <- ggplot(data = titanic, aes(x = product(Class, Sex, Survived))) +
  mosaic_settings(
    expected = ~ Class + Sex + Survived + Class:Survived + Sex:Survived
  ) +
  geom_mosaic() +
  scale_fill_residual(limits = c(-10, 10)) +
  geom_mosaic_text(display_values = "residual",
                   format_digits = 1,
                   size = 2.5) +
  labs(title = "Class ⊥ Sex | Survival",
       subtitle = "~ Class + Sex + Survived + Class:Survived + Sex:Survived") +
  theme_mosaic()

print(p1)

print(p2)

print(p3)

Each model tells a different story about the associations in the data.

References

  • Friendly, M. (1994). “Mosaic Displays for Multi-Way Contingency Tables.” Journal of the American Statistical Association, 89(425), 190-200.
  • Hartigan, J. A., & Kleiner, B. (1981). “Mosaics for Contingency Tables.” Computer Science and Statistics: Proceedings of the 13th Symposium on the Interface, 268-273.
  • Meyer, D., Zeileis, A., & Hornik, K. (2006). “The Strucplot Framework: Visualizing Multi-way Contingency Tables with vcd.” Journal of Statistical Software, 17(3), 1-48.
  • Zeileis, A., Meyer, D., & Hornik, K. (2007). “Residual-based Shadings for Visualizing (Conditional) Independence.” Journal of Computational and Graphical Statistics, 16(3), 507-525.

Session Info

sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.5 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
#>  [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
#>  [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
#> [10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   
#> 
#> time zone: UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] dplyr_1.2.1     ggmosaic2_0.5.1 ggplot2_4.0.3  
#> 
#> loaded via a namespace (and not attached):
#>  [1] gtable_0.3.6        jsonlite_2.0.0      compiler_4.6.1     
#>  [4] Rcpp_1.1.2          tidyselect_1.2.1    tidyr_1.3.2        
#>  [7] jquerylib_0.1.4     productplots_0.1.2  systemfonts_1.3.2  
#> [10] scales_1.4.0        textshaping_1.0.5   yaml_2.3.12        
#> [13] fastmap_1.2.0       plyr_1.8.9          R6_2.6.1           
#> [16] labeling_0.4.3      generics_0.1.4      knitr_1.52         
#> [19] htmlwidgets_1.6.4   ggrepel_0.9.8       tibble_3.3.1       
#> [22] desc_1.4.3          bslib_0.12.0        pillar_1.11.1      
#> [25] RColorBrewer_1.1-3  rlang_1.3.0         cachem_1.1.0       
#> [28] xfun_0.60           fs_2.1.0            sass_0.4.10        
#> [31] S7_0.2.2            otel_0.2.0          viridisLite_0.4.3  
#> [34] plotly_4.12.1       cli_3.6.6           pkgdown_2.2.1      
#> [37] withr_3.0.3         magrittr_2.0.5      digest_0.6.39      
#> [40] grid_4.6.1          lifecycle_1.0.5     vctrs_0.7.3        
#> [43] data.table_1.18.6.1 evaluate_1.0.5      glue_1.8.1         
#> [46] farver_2.1.2        ragg_1.5.2          purrr_1.2.2        
#> [49] httr_1.4.9          rmarkdown_2.32      tools_4.6.1        
#> [52] pkgconfig_2.0.3     htmltools_0.5.9