A Data Frame Is a List of Columns
A data frame is R's table: rows of observations, columns of variables. It's the spreadsheet of R, and the shape practically all real data arrives in - one row per person, per measurement, per transaction; one column per attribute.
Structurally, a data frame is a list of equal-length vectors with row/column behavior layered on top. That one fact explains most of its rules: each column has a single type (it's a vector), different columns can have different types (it's a list), and every column must be the same length (that's the "frame" part).
You build one by handing named vectors to data.frame():
Text, numbers, and logicals living side by side - the mix a matrix can't hold. In practice you'll rarely type data in by hand like this; tables usually arrive via read.csv or a database. But everything below applies identically no matter where the data frame came from.
Sizing Up a Data Frame
Never compute on a dataset you haven't looked at. R's inspection toolkit is short and worth making reflexive:
str()is the one to run first on any freshly loaded data: one line per column with its type and first values. It's where you catch the column of "numbers" that actually read in as character because of one stray entry.nrow()/ncol()give the dimensions;names()lists the column names.summary()gives per-column statistics - min/median/mean/max for numbers, counts for logicals and factors.head(df)andtail(df)print the first and last six rows - indispensable once real datasets get too big to print whole. You'll seehead()in action below on a built-in dataset.
Three Ways to Get a Column
Column access is the bread-and-butter operation, and R gives you three spellings:
people$age- the everyday form: short, readable, autocompletes in most editors.people[["age"]]- identical result, but the name can come from a variable (col <- "age"; people[[col]]), which$can't do. Prefer it inside functions.people[, "age"]- matrix-style: rows before the comma (empty = all), columns after.
All three return the column as a plain vector, ready for mean(people$age). The fourth spelling in the snippet is the trap: people["age"] with single brackets returns a one-column data frame, not a vector - the same [ vs [[ distinction lists have, because a data frame is a list. If a math function complains that your input isn't numeric, check your brackets.
Rows and Cells
The [row, column] notation reaches any slice of the table:
people[2, ] is the whole second row (as a one-row data frame - rows keep their frame, since they mix types). people[2, "name"] is a single cell. A vector of row numbers pulls several rows.
The last line is the important one: a logical condition on a column selects rows - here, everyone above 28. This mask-the-rows pattern is the foundation of all data filtering in R, and it deserves its own article: see filter and subset for the full treatment, including subset() and multi-condition filters.
Adding and Removing Columns
Because a data frame is a list of columns, columns come and go by plain assignment:
Assigning to a new name adds the column; the vector must match nrow() (or be length 1, which recycles to fill every row). The second addition shows the idiomatic move: new columns computed from existing ones, using vectorized math across the whole column at once. Assigning NULL deletes a column entirely.
Column values can be overwritten the same way - people$age <- people$age + 1 ages everyone a year.
Built-in Datasets, and a Word on Tibbles
R ships with practice datasets preloaded, so you can try everything above without importing a thing. The two you'll meet in every tutorial are mtcars (32 cars, 11 numeric variables) and iris (150 flowers, 4 measurements plus a species factor):
head() earning its keep: six rows tell you the shape of the thing without flooding the screen. These datasets are ideal playgrounds - when you're unsure how some function behaves, test it on mtcars before pointing it at your own data.
One term you'll meet as soon as you touch the tidyverse: the tibble, the tidyverse's take on the data frame. Same concept, politer behavior - printing shows only the first ten rows with column types, and there's no partial name matching. Created with tibble() or returned by readr/dplyr functions:
library(tibble)
tibble(name = c("Rosa", "Ken"), age = c(30, 41))
#> # A tibble: 2 x 2
#> name age
#> <chr> <dbl>
#> 1 Rosa 30
#> 2 Ken 41
Everything in this article - $, str(), adding columns, logical row masks - works on tibbles unchanged. A tibble is a data frame with extra polish; learn data frames and you've learned both.
What You Take Away
- A data frame is a list of equal-length columns: one type per column, mixed types across the table - R's spreadsheet.
- Inspect before you compute:
str()first, thenhead(),summary(),nrow()/ncol()/names(). df$col,df[["col"]], anddf[, "col"]all return a column as a vector; single-bracketdf["col"]returns a data frame.df[rows, cols]slices anything; a logical mask before the comma filters rows.- Add columns by assignment (often computed from other columns); remove with
NULL; practice onmtcars.
Next up: missing values - what NA means, why it spreads through calculations, and how to handle it.
Frequently Asked Questions
How do you create a data frame in R?
Pass named, equal-length vectors to data.frame(): df <- data.frame(name = c("Rosa", "Ken"), age = c(30, 41)). Each vector becomes a column. In practice most data frames aren't typed in by hand - they arrive from read.csv() or a database - but the structure is the same.
How do you access a column in an R data frame?
Three ways: df$age (quick and readable), df[["age"]] (same result, works when the column name is stored in a variable), and df[, "age"] (matrix-style). All three return the column as a plain vector. df["age"] with single brackets returns a one-column data frame instead - a frequent source of confusion.
What does str() show for a data frame?
One line per column: its name, type, and the first few values, plus the row and column counts at the top. It's the fastest way to see whether a column read in as numeric or text, which is why str() should be the first thing you run on any freshly loaded dataset.
How do you add a column to a data frame in R?
Assign to a name that doesn't exist yet: df$score <- c(88, 75, 93). The vector must match the number of rows (or be length 1, which recycles to every row). Remove a column by assigning NULL: df$score <- NULL.