Categorical Summaries and Cross-Tabulation in R

Context: FIT2086_MOC · summarising categorical variables, where mean/sd are meaningless · the cat–cat half of Association Between Variables · extends R Toolkit (Cheatsheet) · studio data: heart.csv, Mushroom.csv Problem it solves: count the levels of a categorical variable, and detect whether two categorical variables are associated.

Quick Revision

  • 🎯 Trigger: a column of labels (or numeric codes standing for labels) ➔ make it a factor, then table() it; two such columns ➔ table(a, b) for a contingency table.
  • ⚡ Key Constraint: association is read from how the row distribution changes across columns, not from raw cell counts — a big cell in a big row means nothing. Convert to proportions before judging.

🔧 Minimal Working Example

mush <- read.csv("Mushroom.csv", header = TRUE, stringsAsFactors = TRUE)
 
tab <- table(mush$cap.shape)     # one-way frequency table
pie(tab)                          # relative frequencies, visually
 
table(mush$class, mush$odor)      # contingency table: rows = class, cols = odor

Expected output (n = 8124; 4208 edible, 3916 poisonous):

classalmondanisecreosotefishyfoulmustynonepungentspicy
edible4004000000340800
poisonous00192576216036120256576
  • Reading it ➔ every odour except none is entirely one class ➔ odor is by far the strongest predictor of class in this dataset; knowing the odour almost determines edibility.
  • Contrast a weak variabletable(mush$class, mush$cap.shape) splits convex / and flat / — close to the overall base rate ⟹ little association, though bell (/) leans edible and knobbed (/) leans poisonous.

🔀 Variations

Recoding numeric codes as factors

heart <- read.csv("heart.csv", header = TRUE, stringsAsFactors = TRUE)
table(heart$SEX)          # 0: 97, 1: 206  — unreadable without labels
 
heart$SEX <- factor(heart$SEX, labels = c("MALE","FEMALE"), levels = c(0,1))
heart$HD  <- factor(heart$HD,  labels = c("NO","YES"),      levels = c(0,1))
table(heart$SEX, heart$HD)

Expected output (n = 303):

NOYES
MALE (code 0)7225
FEMALE (code 1)92114
  • levels = the values present, labels = what to call them ➔ positionally paired; the -th label names the -th level.
  • Why bothertable() on a raw numeric column prints bare codes; a factor prints meaning, and R will treat it as categorical in every later model.

Proportions instead of counts

tab <- table(mush$class)
prop.table(tab)                       # overall proportions: 0.518 edible, 0.482 poisonous
prop.table(table(mush$class, mush$odor), margin = 2)   # column-wise proportions
  • margin = 1 rows sum to 1 · margin = 2 columns sum to 1 · omitted the whole table sums to 1 ➔ pick the margin that answers the question (“given the odour, how likely is poison?” = column-wise).

✍️ Practice

⚠️ Common Mistakes

  • 💡 Comparing raw counts across unequal groups ➔ a larger group produces larger cells everywhere; always prop.table(..., margin=) before claiming association.
  • 💡 labels/levels is an assertion, not a lookup ➔ R applies whatever mapping you give it, silently. Get the coding backwards and every downstream table, plot and model is mislabelled while looking perfectly plausible — verify the codebook first.
  • 💡 Forgetting stringsAsFactors = TRUE ➔ text columns load as plain character, so R will not treat them as categorical; specify it on every read.csv of a dataset with labels.
  • 💡 pie() for many levels ➔ human eyes compare angles badly; a pie with 9 slices (mushroom odor) hides exactly the differences the table makes obvious (see Data Visualisation (Chart Types)).