Skip to content

Data Wrangling with dplyr and tidyr Interview Questions for R Programming

25 Data Wrangling with dplyr and tidyr questions with answers for r programming roles — 7 easy, 13 medium, 5 hard.

About Data Wrangling with dplyr and tidyr

Most analyst time goes into cleaning and shaping data. Learn the dplyr verbs, joins, window functions, across(), case_when(), pivoting with tidyr, deduplication, imputation and outlier handling. Senior questions cover tidy evaluation in functions and scaling pipelines with dbplyr, dtplyr, duckdb and arrow when data no longer fits in memory.

Sheet progressStart practicing
3 QUESTIONS FREESign up to unlock the full question bank and track your progress.Unlock every question and answer, then continue practising company-wise and package-wise.
3 questions free Unlock the rest in seconds
01

What is the tidyverse?

Save to ▾
Show answer
The tidyverse is a collection of R packages that share a common design and grammar for data science. Core packages include readr for importing, dplyr for transformation, tidyr for reshaping, ggplot2 for visualization, purrr for functional programming, stringr for text, forcats for factors and lubridate for dates. library(tidyverse) loads the core set. Its consistent function names and the pipe make analysis code readable, which is why most R analytics teams use it.
02

What are the main dplyr verbs?

Save to ▾
Show answer
dplyr has a small set of verbs for data transformation: filter() keeps rows that meet conditions, select() picks columns, mutate() creates or changes columns, arrange() sorts rows, summarise() reduces groups to summary rows and group_by() defines groups for the next operations. Others include distinct(), rename(), relocate(), count() and slice(). Each takes a data frame as the first argument and returns a data frame, so verbs chain naturally with the pipe.
03

What are the principles of tidy data?

Save to ▾
Show answer
Tidy data follows three rules: each variable is a column, each observation is a row and each type of observational unit forms a table. For example, a sales sheet with separate columns for Jan, Feb and Mar is not tidy, because month is a variable spread across columns. Reshaping it into month and sales columns makes it tidy. Tidy data works directly with dplyr, ggplot2 and modelling functions, reducing reshaping effort later.
04

Write dplyr code to find total and average sales by city, sorted by total.

Save to ▾
Show answer
Group, summarise and sort: library(dplyr) city_summary <- sales %>% filter(!is.na(amount)) %>% group_by(city) %>% summarise( orders = n(), total = sum(amount), avg_order = mean(amount), .groups = 'drop' ) %>% arrange(desc(total)) head(city_summary, 10)
05

How do you split one column into several, or combine columns into one?

Save to ▾
Show answer
tidyr provides separate_wider_delim() to split and unite() to combine. library(tidyr) df <- tibble(full_name = c('Amit Kumar', 'Neha Singh'), dob = c('12-05-1999', '03-11-2000')) df %>% separate_wider_delim(full_name, delim = ' ', names = c('first', 'last')) df %>% separate_wider_delim(dob, '-', names = c('d', 'm', 'y')) %>% unite('dob_iso', y, m, d, sep = '-')
06

Finance says 120 transactions are missing from your report compared with their system. How do you find them?

Save to ▾
Show answer
I load both datasets and standardise the key, for example trimming spaces and matching data types for transaction_id. Then anti_join(finance, report, by = 'transaction_id') lists transactions in finance but not in my report, and the reverse anti_join shows extras. I examine the missing rows for patterns such as a date range, status or filter in my code. Usually the cause is a timezone cut-off or an excluded status.
07

What is the difference between mutate() and summarise()?

Save to ▾
Show answer
mutate() adds or modifies columns and keeps the same number of rows, so each row gets a value. summarise() collapses each group into one row of summary values. For example, group_by(city) %>% mutate(city_avg = mean(sales)) adds the city average to every row, useful for comparing each row with its group, while group_by(city) %>% summarise(city_avg = mean(sales)) returns one row per city.
08

What is the difference between %>% and |> in R?

Save to ▾
Show answer
%>% comes from the magrittr package and |> is the native pipe built into R since version 4.1. Both pass the left side as the first argument of the right side, so df |> filter(x > 1) equals filter(df, x > 1). The native pipe is faster and needs no package, but it uses _ as a named placeholder only, while %>% supports the dot placeholder anywhere. New code increasingly uses |>, and both work with dplyr.
09

How do you create a column with multiple conditions in dplyr?

Save to ▾
Show answer
case_when() evaluates conditions in order and returns the value of the first one that is TRUE; .default handles the rest. customers <- customers %>% mutate(segment = case_when( spend >= 50000 ~ 'Platinum', spend >= 20000 ~ 'Gold', spend >= 5000 & visits > 5 ~ 'Silver', .default = 'Regular' )) # simple two-way split mutate(df, high_value = if_else(spend > 20000, 'Yes', 'No'))
10

Explain the different types of joins in dplyr.

Save to ▾
Show answer
left_join() keeps all rows of the left table and adds matching columns from the right, inner_join() keeps only matching rows, right_join() keeps all rows of the right table and full_join() keeps all rows from both, filling NA where there is no match. Keys are given with by = join_by(customer_id) or by = c('id' = 'cust_id') for different names. Left joins are most common when enriching a fact table with lookup details.
11

How do you reshape data between wide and long formats in R?

Save to ▾
Show answer
tidyr's pivot_longer() turns columns into rows and pivot_wider() does the reverse. library(tidyr) # wide: product, Jan, Feb, Mar long <- sales_wide %>% pivot_longer(cols = Jan:Mar, names_to = 'month', values_to = 'sales') wide <- long %>% pivot_wider(names_from = month, values_from = sales, values_fill = 0)
12

How do you apply the same transformation to many columns in dplyr?

Save to ▾
Show answer
across() applies one or more functions to a set of columns selected with tidyselect helpers. df %>% mutate(across(where(is.character), str_trim)) %>% mutate(across(starts_with('amt_'), ~ round(.x, 2))) df %>% group_by(region) %>% summarise(across(c(sales, profit), list(mean = mean, max = max), .names = '{.col}_{.fn}'))
13

What window functions does dplyr provide?

Save to ▾
Show answer
Window functions return one value per row while looking at other rows in the group. Common ones are lag() and lead() for previous or next values, row_number(), min_rank() and dense_rank() for ranking, cumsum() and cummean() for running totals, and ntile() for buckets. For example, group_by(product) %>% arrange(month) %>% mutate(prev = lag(sales)) gives the previous month's sales per product, which is the basis for growth calculations.
14

You need month-over-month sales growth for each product. How do you calculate it in R?

Save to ▾
Show answer
I first aggregate to one row per product and month with group_by() and summarise(). Then I group by product, arrange by month and use mutate(growth = (sales - lag(sales)) / lag(sales) * 100). The first month for each product becomes NA, which is correct. I make sure months with zero sales exist using tidyr::complete(), otherwise lag() compares non-consecutive months. Finally I round and format growth for reporting.
15

A customer table has multiple records per customer. How do you keep only the latest record for each?

Save to ▾
Show answer
I group by customer_id and keep the row with the latest updated_at using slice_max(updated_at, n = 1, with_ties = FALSE), then ungroup. An alternative is arrange(desc(updated_at)) followed by distinct(customer_id, .keep_all = TRUE). I check that updated_at is a proper date-time and has no NAs, since missing timestamps would be dropped or mis-sorted. Finally I confirm that n_distinct(customer_id) equals the number of rows.
16

Salary data has a few extreme values that distort averages. How do you handle them?

Save to ▾
Show answer
First I investigate whether they are data errors, such as an extra zero, or genuine high earners. I flag outliers with the IQR rule, values beyond Q1 - 1.5 IQR or Q3 + 1.5 IQR, or with percentiles. Errors I fix or remove with documentation. Genuine values I keep, but I report the median alongside the mean, or winsorize at the 1st and 99th percentiles for modelling. The choice depends on the business question.
17

How would you impute missing values differently for each group?

Save to ▾
Show answer
I group by the relevant category, such as product category, and replace NAs with that group's median: group_by(category) %>% mutate(price = coalesce(price, median(price, na.rm = TRUE))) %>% ungroup(). Group medians are more realistic than a global value. I also add a flag column marking imputed rows so models and reviewers can see them. If a whole group is missing, I fall back to the overall median, and for important features I consider model-based imputation.
18

What are semi_join() and anti_join() used for?

Save to ▾
Show answer
They are filtering joins: they keep rows of the left table based on matches in the right table but add no columns. semi_join() keeps rows that have a match, and anti_join() keeps rows that have no match. For example, anti_join(customers, orders, by = 'customer_id') lists customers who never ordered. They are also safer than inner joins for filtering, since they never duplicate rows when the right table has repeated keys.
19

Why is ungroup() important in dplyr?

Save to ▾
Show answer
A grouped data frame stays grouped after mutate() and some other verbs, so later operations silently run per group. For example, a mutate(share = sales / sum(sales)) meant to compute share of grand total would compute share within each group instead. Calling ungroup() after grouped work, or using .groups = 'drop' in summarise() or the .by argument for one-off grouping, prevents these subtle bugs in long pipelines.
20

Tell me about the messiest dataset you have cleaned.

Save to ▾
Show answer
Situation: I received three years of vendor invoices from different branches in Excel files with inconsistent headers, merged cells and dates in four formats. Task: build one clean table for spend analysis. Action: I used readxl to read all sheets, janitor::clean_names() to standardise headers, lubridate to parse dates and a mapping table for vendor name variants. I wrote validation checks for totals. Result: a clean dataset of 180,000 rows that finance now refreshes monthly.
21

Your dplyr pipeline is too slow on a 50 million row dataset. What are your options?

Save to ▾
Show answer
I can keep dplyr syntax but change the backend. dtplyr translates dplyr code to data.table, which is much faster in memory. duckdb with dbplyr runs the pipeline in an embedded columnar database, often handling data larger than RAM. arrow lets me query Parquet files with dplyr verbs lazily. I would also filter and select early, avoid row-wise operations and profile the pipeline to find the slow step before rewriting anything.
22

What is dbplyr and how does it work?

Save to ▾
Show answer
dbplyr is a dplyr backend that translates dplyr verbs into SQL and runs them inside a database. You connect with DBI, reference a table with tbl(con, 'orders') and write normal dplyr code. Queries are lazy: nothing runs until you call collect(), and show_query() prints the generated SQL. This lets analysts work with large warehouse tables without pulling all rows into R, though some R functions have no SQL translation.
23

What is tidy evaluation and why do you need {{ }} in functions?

Save to ▾
Show answer
dplyr uses data masking, so you can write column names without quotes. Inside your own function, a column passed as an argument is not found automatically because R looks for a variable with that name. Embracing the argument with {{ }} tells dplyr to use the column the caller supplied. For column names stored as strings, you use .data[[var]]. For new column names, use the glue syntax '{{var}}_mean' := with the walrus operator.
24

Write a reusable function that summarises any numeric column by any grouping column using dplyr.

Save to ▾
Show answer
Use {{ }} to pass columns into data-masking verbs: library(dplyr) summarise_by <- function(data, group_col, value_col) { data %>% group_by({{ group_col }}) %>% summarise( n = n(), mean_val = mean({{ value_col }}, na.rm = TRUE), total = sum({{ value_col }}, na.rm = TRUE), .groups = 'drop' ) %>% arrange(desc(total)) } summarise_by(sales, city, amount)
25

Describe a mistake you made in an analysis and how you handled it.

Save to ▾
Show answer
Situation: I shared a retention analysis showing improvement, but a week later I noticed I had excluded customers with missing signup dates, who were mostly churned users. Task: correct it before decisions were made. Action: I informed my manager immediately, reran the analysis with proper handling, explained the impact clearly and added a missing-value check to my template. Result: the corrected numbers were used, and my manager appreciated the transparency.

Frequently asked questions

How important is Data Wrangling with dplyr and tidyr in R Programming interviews?

Most analyst time goes into cleaning and shaping data. Learn the dplyr verbs, joins, window functions, across(), case_when(), pivoting with tidyr, deduplication, imputation and outlier handling. Senior questions cover tidy evaluation in functions and scaling pipelines with dbplyr, dtplyr, duckdb and arrow when data no longer fits in memory.

How should I use the Data Wrangling with dplyr and tidyr sheet?

Go top to bottom — easy to hard. Answer before opening each answer, tick what you solved and save misses to Last-Minute Revision.