Skip to course content
Free R data course

R Foundations for Data Analysis

Unit 03.00: One rule turns a list into a table

A data frame is not a new kind of object. It is a list with one rule attached, and that rule is what makes it a table.

One rule turns a list into a table

A list holds anything, of any lengths โ€” a string here, a vector of two numbers there. A data frame is a list whose elements are all the same length, which is why they line up as columns and why 'row 3' means something.

A tibble is a data frame with the historical surprises removed. Two matter early on. Base data frames do partial matching, so df$vis silently returns the visits column โ€” convenient until a column called visitors arrives and the meaning changes. And selecting a single column from a base data frame drops it to a vector, so code that expected a table breaks one step later.

Tibbles refuse both. Everything else about them is the same.

This block builds all three and compares their behaviour.


suppressPackageStartupMessages(library(tibble))

# A list holds anything, of any length.
record <- list(ward = "North", visits = c(412, 455), staffed = TRUE)
cat("List element lengths:", paste(lengths(record), collapse = " "), "\n\n")

# A data frame is a list whose elements are all the same length.
# That single constraint is what makes it a table.
base_df <- data.frame(ward = c("North", "South"), visits = c(412, 388))
tbl <- tibble(ward = c("North", "South"), visits = c(412, 388))

cat("data.frame is built on a list:", is.list(base_df), "\n")
cat("All columns equal length     :", length(unique(lengths(base_df))) == 1, "\n\n")

# A tibble is a data frame with the surprises removed.
cat("data.frame partial matching, base_df$vis :", base_df$vis, "\n")
cat("tibble refuses to guess,      tbl$vis     :",
    if (is.null(tbl$vis)) "NULL" else tbl$vis, "\n\n")

cat("Single-column selection from a data.frame drops to a vector:",
    class(base_df[, "visits"]), "\n")
cat("From a tibble it stays a table:", class(tbl[, "visits"])[1], "\n")

The list holds elements of lengths 1, 2 and 1, so it is not a table. The data frame is confirmed to be a list underneath with all columns equal length. Then the differences: base_df$vis returns the visits column by partial match, while tbl$vis returns NULL because the tibble will not guess. And single-column selection gives class numeric from the data frame but tbl_df from the tibble โ€” still a table.

The mistake this prevents

The mistake is relying on partial matching without knowing you are. It works for months and then breaks the day somebody adds a column whose name shares a prefix.

Takeaway

Use tibbles for new work. Expect base data frames from older code and from some packages, and remember their two habits: partial name matching, and dropping to a vector on single-column selection.