Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Manipulating data in R means applying a deliberate sequence of transformations to a data frame: keep the rows and columns you need, create or change variables, order records, and reduce groups to summaries. The example below uses dplyr to make each step explicit, then shows the equivalent base R approaches so you can choose the style and execution path that fit your project.

Start with a small, explicit example

Suppose sales contains one row per order:

sales <- data.frame(
  order_id = c(101, 102, 103, 104),
  region = c("North", "South", "North", "South"),
  product = c("A", "A", "B", "B"),
  units = c(3, 5, 2, 8),
  price = c(10, 10, 15, 15),
  returned = c(FALSE, FALSE, TRUE, FALSE)
)

Our target is a table of non-returned orders, with a calculated revenue column, sorted from largest to smallest sale. We can express that result as a left-to-right pipeline:

library(dplyr)

result <- sales |>
  filter(!returned) |>
  mutate(revenue = units * price) |>
  select(order_id, region, product, units, revenue) |>
  arrange(desc(revenue))

The pipe (|>) passes the result of each operation to the next one. Assigning the pipeline to result is what preserves the transformed data; printing a pipeline alone does not replace the original object.

Inspect data before transforming it

Many transformation errors come from incorrect assumptions about names, types, missing values, or row counts. Check the input first:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
names(sales)
str(sales)
head(sales)
sum(is.na(sales))
nrow(sales)
  • Names: confirm spelling and capitalization.
  • Types: verify that numbers are numeric, dates are dates, and categories have the intended representation.
  • Missing values: decide whether to remove, retain, or explicitly handle NA values.
  • Row count: record the starting size so filtering and joins can be checked later.

Filter rows with filter()

filter() keeps cases whose conditions evaluate to TRUE. Multiple comma-separated conditions are combined with logical AND.

sales |> filter(region == "North")
sales |> filter(units >= 5, !returned)
sales |> filter(product %in% c("A", "B"))

Use is.na() and !is.na() when missing values matter. A comparison such as price > 10 returns NA for a missing price, so that row will not pass the filter unless you handle it explicitly.

Choose columns with select()

select() returns only the variables needed for the next task or final output.

sales |> select(order_id, region, revenue)
sales |> select(-returned)
sales |> select(starts_with("unit"), ends_with("price"))

Reducing columns early can make later code easier to read and prevents accidental use of irrelevant fields.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Order rows with arrange()

sales |> arrange(region, product)
sales |> arrange(desc(units), product)

Ordering does not change the values or remove rows; it changes their display and stored order. Use desc() for descending order and add a second variable to make ties deterministic.

Create and modify variables with mutate()

mutate() adds columns or replaces existing ones. New expressions can use columns created earlier in the same call.

sales |>
  mutate(
    revenue = units * price,
    net_revenue = if_else(returned, 0, revenue),
    high_value = revenue >= 75
  )

For more than two possible outcomes, use case_when():

sales |>
  mutate(
    tier = case_when(
      revenue >= 100 ~ "large",
      revenue >= 50 ~ "medium",
      TRUE ~ "small"
    )
  )

Make sure the replacement expression has a compatible type. Also decide how missing inputs should propagate; arithmetic involving NA normally produces NA unless you supply an explicit rule.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Group and summarize data

group_by() changes the meaning of subsequent operations. summarise() then reduces each group to summary rows.

regional_sales <- sales |>
  filter(!returned) |>
  mutate(revenue = units * price) |>
  group_by(region) |>
  summarise(
    orders = n(),
    units_sold = sum(units),
    revenue = sum(revenue),
    average_order = mean(revenue),
    .groups = "drop"
  )

With one grouping variable, the output has one row per region; with several, it has one row for each combination. The .groups argument controls whether grouping is dropped, kept, or simplified after summarising. This matters when another operation follows, because a still-grouped data frame can produce grouped rather than overall results. Backend implementations can differ, so verify grouping after a summary when working beyond a local data frame.

Inspect the result rather than assuming the aggregation did what you intended:

nrow(regional_sales)
regional_sales
group_vars(regional_sales)

Join related tables as a separate task

Transformation often requires combining tables, such as orders and a product lookup. Joins match keys; they are not substitutes for row filtering or summarising.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
orders |> left_join(products, by = "product_id")
orders |> inner_join(products, by = "product_id")
orders |> full_join(products, by = "product_id")
  • Left join: keeps every row from the first table and adds matches.
  • Inner join: keeps only rows with a match in both tables.
  • Full join: keeps all rows from both tables, using missing values where no match exists.

Before and after a join, check row counts and key uniqueness. A duplicated key on either side can legitimately multiply rows, while an unexpected key mismatch can introduce missing columns. dplyr documents joins and set operations as a distinct two-table verbs topic.

A complete, checkable workflow

  1. Define the desired shape. Write down whether the output should retain individual records or contain one row per group.
  2. Inspect the input. Check names, types, missing values, key columns, and nrow().
  3. Filter and select. Remove irrelevant cases and variables before more complex work.
  4. Mutate. Create calculated fields and document rules for missing or exceptional values.
  5. Arrange or summarise. Order detail rows, or group and reduce them to the intended grain.
  6. Join deliberately. Confirm key uniqueness and compare row counts before and after.
  7. Validate the output. Inspect a sample, names, types, missingness, grouping, and summary totals.
  8. Save the result. Assign it to an object or write it to the required file or database table.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

dplyr compared with base R

Both approaches are valid. dplyr offers a consistent data-frame grammar and named verbs; base R uses indexing and a collection of vector, data-frame, and statistical functions. The correspondence is practical rather than perfectly identical in every edge case.

Task dplyr Typical base R equivalent
Keep rows filter(df, x > 0) df[df$x > 0, ] or subset(df, x > 0)
Choose columns select(df, a, b) df[c("a", "b")]
Add a column mutate(df, z = x + y) df$z <- df$x + df$y or transform(df, z = x + y)
Sort rows arrange(df, desc(x)) df[order(-df$x), ]
Remove duplicate rows distinct(df) unique(df)
Summarise by group group_by(df, g) |> summarise(m = mean(x)) aggregate(x ~ g, df, mean) or tapply(df$x, df$g, mean)

Choose based on the task and environment:

  • Readability and team conventions: dplyr’s verbs and pipelines make a multi-step transformation read from left to right; a base-R codebase may be clearer when it consistently uses indexing and standard functions.
  • Dependencies: base R avoids an additional package dependency. dplyr requires installation and loading, but gives a coherent vocabulary.
  • Grouped work: dplyr makes grouping state explicit with group_by(); base R offers several capable functions with different interfaces.
  • Data location and scale: ordinary data frames run in memory. For larger-than-memory or remote data, dplyr-related options include Arrow, dbplyr for relational databases, dtplyr for large in-memory data, duckplyr for DuckDB, and sparklyr for Spark. These are backend choices, not automatic guarantees of a particular speed improvement.

Common failure modes and recovery checks

  • Unexpected zero rows: print the condition’s inputs and check spelling, factor or character values, and NA handling.
  • Too many rows after a join: test whether the join key is unique in each table and inspect unmatched keys.
  • Wrong summary grain: check group_vars() and confirm that each output row represents the intended group.
  • Missing calculated values: inspect source columns for NA and decide whether a default or an omission rule is appropriate.
  • Changes do not persist: assign the result to an object; a pipeline that is only printed has not overwritten the input.

Further learning

The official dplyr documentation covers introductions, grouped data, two-table verbs, column-wise and row-wise operations, programming, and a comparison with base R. New users are also directed to the data-transformation chapter of R for Data Science.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.