Data manipulation with base R

Select, filter, arrange, mutate, group and join without leaving base R — and the exact dplyr verb each base expression replaces.

Selecting, filtering and ordering

df <- data.frame(
  city  = c("oslo", "oslo", "bergen", "bergen", "oslo"),
  month = c(1, 2, 1, 2, 3),
  sales = c(120, 90, 200, 150, 60),
  stringsAsFactors = FALSE
)

df[c("city", "sales")]                     # select columns, always a data frame
df[["sales"]]                              # one column as a vector

df[df$sales > 100, ]                       # the comma is required, and so is the space
subset(df, sales > 100 & city == "oslo")   # reads better but silently drops NA rows

df[order(df$city, -df$sales), ]            # arrange: order() returns a permutation

df$per_day <- df$sales / 30                # mutate in place
copy <- transform(df, band = ifelse(sales > 100, "big", "small"))

renamed <- df
names(renamed)[names(renamed) == "city"] <- "town"

rm(df)   # base R has no verb for this; names are vectors you edit directly
  • subset() is convenient but drops rows whose condition evaluates to NA, and it is documented as unsuitable inside functions — use [ for library code.
  • order() returns positions, not sorted values. You must re-index the data frame with it.
  • Prefix a numeric column with a minus inside order() to sort descending.
  • transform() returns a new data frame and can see the columns you just defined in the same call.

Grouping, summarising and joining

aggregate(sales ~ city, data = df, FUN = mean)
aggregate(cbind(sales, month) ~ city, data = df, FUN = sum)   # several columns at once

# one FUN that returns a vector gives a matrix column; flatten it
agg <- aggregate(sales ~ city, data = df,
                 FUN = function(v) c(n = length(v), mean = mean(v), max = max(v)))
do.call(data.frame, agg)

sapply(split(df$sales, df$city), mean)     # split, apply, combine
tapply(df$sales, df$city, max)             # named vector result
ave(df$sales, df$city, FUN = mean)         # vector the same length as df

lookup <- data.frame(city = c("oslo", "bergen"),
                     country = c("NO", "NO"), stringsAsFactors = FALSE)

merge(df, lookup, by = "city", all.x = TRUE)   # left join
merge(df, lookup, by = "city", all = TRUE)     # full outer join
merge(df, lookup, by = "city", all.y = TRUE)   # right join
dplyr verbBase R equivalentNote
filter(df, x > 1)df[df$x > 1, ] or subset()subset() drops NA rows
select(df, a, b)df[c("a", "b")]No tidy-select helpers in base R
arrange(df, x)df[order(df$x), ]order() is a permutation; you re-index
mutate(df, y = x * 2)df$y <- x * 2, transform()transform copies, $<- modifies in place
group_by() then summarise()aggregate, tapply, aveaggregate takes one FUN, which may return a vector
left_join(a, b)merge(a, b, all.x = TRUE)merge sorts by the key by default
rename(df, y = x)names(df)[names(df) == "x"] <- "y"No rename helper; edit the names vector
slice(df, 1:5)head(df, 5)head is the idiomatic base answer
distinct(df)unique(df)unique works on the rows of a data frame
count(df, g)table(df$g) or aggregate with lengthtable returns a contingency table
⚠️
merge sorts its result by the key, so a join quietly reorders your rows — carry an explicit index column or re-sort afterwards if order matters. It also expands anything ambiguous: duplicate keys on both sides produce the full cross product, which is how a join turns 500 rows into 50 000.

FAQ

Should I learn base R or dplyr?
Learn base R first: it is always available, it is what packages return, and the verbs map one-to-one onto it. Then adopt dplyr for readability on wide pipelines. Knowing the mapping in the table above means you can read either style.
Why does sapply return a character matrix?
Because sapply simplifies its result, and a data frame with mixed column types becomes a matrix — which can hold only one type, so everything is coerced to character. Use lapply to keep a list, or vapply with an explicit type to fail loudly instead.

Vectors, factors and data frames Grouping, joining and reshaping

Last refreshed 2026-09-18.