Skip to content

Hard R Programming Interview Questions

45 hard questions with answers. Design, trade-off and deep-dive questions asked for senior roles and at top product companies.

Topics in the hard set

Statistics and Hypothesis Testing: 8 Functions and Functional Programming: 7 Data Visualization with ggplot2: 6 Modelling and Machine Learning in R: 6 R Basics and Data Types: 5 Data Wrangling with dplyr and tidyr: 5 Data Structures in R: 4 Reports, Shiny and Production R: 4
Solved — hardStart 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

Explain environments and lexical scoping in R.

Save to ▾
Show answer
An environment is a collection of name-value bindings with a pointer to a parent environment. R uses lexical scoping: when a function looks up a variable, it searches its own environment, then the environment where it was defined, up through the global environment and attached packages. So a function defined inside another can read the outer function's variables, which enables closures. The <<- operator modifies a binding in a parent environment and should be used sparingly.
02

What is lazy evaluation in R?

Save to ▾
Show answer
R evaluates function arguments lazily, only when they are first used inside the function body. Arguments are stored as promises holding the expression and its environment. For example, f <- function(x, y) x * 2 works with f(5, stop('error')) because y is never touched. Lazy evaluation enables defaults that depend on other arguments and non-standard evaluation in dplyr, but it can surprise you when creating closures in a loop, which force() fixes.
03

What does copy-on-modify mean in R and how does it affect performance?

Save to ▾
Show answer
When you assign y <- x, both names point to the same object, and R makes a copy only when one of them is modified. tracemem() shows when copies happen. This is why growing a vector in a loop with x <- c(x, value) is slow, because each step can copy the whole vector. Preallocating with vector('numeric', n), using vectorized functions or data.table's modify-by-reference avoids repeated copies and memory spikes.
04

Your R session runs out of memory while processing a large dataset. What steps do you take?

Save to ▾
Show answer
I check object sizes with lobstr::obj_size() and remove large intermediates with rm() followed by gc(). I load only the needed columns and use data.table, which modifies by reference without copies. For data larger than RAM, I push computation into a database through dbplyr, or use arrow with Parquet files so filters and aggregations run before data enters R memory. Chunked reading with readr::read_csv_chunked() is another option for one-pass jobs.
05

What are the differences between S3, S4 and R6 classes?

Save to ▾
Show answer
S3 is R's simplest object system: an object is a list with a class attribute, and generic functions like print() dispatch on it through UseMethod(). S4 adds formal class definitions with setClass(), typed slots and validity checks, and is common in Bioconductor. R6, from a package, provides reference semantics with mutable objects and encapsulated methods, similar to classes in Python or Java, and is used inside Shiny. Most analysis code needs only S3.
06

What is a list-column and why is it useful?

Save to ▾
Show answer
A list-column is a data frame column whose cells are lists, such as nested data frames or fitted models. With tidyr::nest() you group data, for example by region, into a column of sub-data frames, then use purrr::map() to fit a model per region and broom::tidy() to extract coefficients. This keeps data, models and results aligned row by row, which is cleaner than managing separate lists of models and names.
07

Write code to fit one regression model per group using nested data.

Save to ▾
Show answer
Fit one model per region with list-columns: library(dplyr); library(tidyr) library(purrr); library(broom) models <- sales %>% group_by(region) %>% nest() %>% mutate( fit = map(data, ~ lm(revenue ~ ad_spend, data = .x)), coefs = map(fit, tidy), r2 = map_dbl(fit, ~ glance(.x)$r.squared) ) models %>% unnest(coefs) %>% select(region, term, estimate)
08

Are factors more memory efficient than character vectors in modern R?

Save to ▾
Show answer
Not by much. R keeps a global string pool, so each unique string is stored once and a character vector only holds pointers, which makes repeated values already compact. Factors still matter for controlling level order in plots and models and for representing a fixed set of categories. For huge data, data.table and arrow handle strings efficiently, and converting high-cardinality IDs to factors usually adds overhead without any benefit.
09

An API returns deeply nested JSON. How do you turn it into a tidy data frame in R?

Save to ▾
Show answer
I make the request with httr2, handling authentication and pagination, then parse with jsonlite::fromJSON(), which simplifies arrays of objects into data frames, using flatten = TRUE to spread nested objects into columns. For deeper nesting I keep a list and use tidyr::unnest_wider() and unnest_longer() to expand list-columns step by step. Optional keys become NA, so I check missing fields across records and validate types before analysis.
10

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.
11

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.
12

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.
13

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)
14

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.
15

Leadership wants a single report page with 15 charts. How do you approach it?

Save to ▾
Show answer
I first ask which decisions the page supports and which three to five metrics matter most, because 15 charts usually hide the message. I put headline KPIs at the top, followed by trend charts and then breakdowns, with consistent colours, scales and a shared theme function. Less important views go to a drill-down tab in a Shiny or Quarto dashboard. I test the draft with one stakeholder and remove charts nobody uses.
16

Why are dual-axis charts discouraged and how does ggplot2 handle secondary axes?

Save to ▾
Show answer
Dual-axis charts plot two measures with different scales on one chart, and the visual relationship between the lines depends on arbitrary axis ranges, so readers can be misled about correlation. ggplot2 only allows a secondary axis that is a one-to-one transformation of the primary axis through sec_axis(), such as Celsius and Fahrenheit. For two different metrics, I prefer two aligned panels using facets or the patchwork package.
17

Rendering a ggplot with 10 million points is extremely slow. What would you do?

Save to ▾
Show answer
I first question whether all points are needed. I aggregate before plotting, for example binning into hexagons with geom_hex() or summarising by time interval. If raw points are required, the scattermore package rasterizes millions of points quickly, and ggrastr rasterizes just the point layer while keeping text as vector. Saving as PNG instead of PDF also helps, because vector files with millions of shapes are huge and slow to open.
18

What is the difference between coord_cartesian() limits and scale limits in ggplot2?

Save to ▾
Show answer
Setting limits with scale_y_continuous(limits = c(0, 100)) or ylim() removes data outside the range before statistics are calculated, which changes boxplots, smooth lines and bar totals and gives warnings about removed rows. coord_cartesian(ylim = c(0, 100)) only zooms the view, keeping all data in calculations. So for zooming into part of a chart without altering the summary, coord_cartesian() is the correct choice.
19

How would you create a reusable branded theme for all team charts?

Save to ▾
Show answer
Wrap theme settings and palettes in functions stored in an internal package: theme_team <- function(base_size = 12) { theme_minimal(base_size = base_size) + theme( plot.title = element_text(face = 'bold', colour = '#1F3A5F'), panel.grid.minor = element_blank(), legend.position = 'top' ) } scale_fill_team <- function(...) { scale_fill_manual(values = c('#1F3A5F', '#F28C28', '#2A9D8F'), ...) } ggplot(df, aes(x, y, fill = g)) + geom_col() + theme_team() + scale_fill_team()
20

How do you create maps in R?

Save to ▾
Show answer
The sf package stores spatial data as data frames with a geometry column, and ggplot2's geom_sf() draws them. You read shapefiles or GeoJSON with st_read(), join your data, for example sales by state, and fill polygons by value to make a choropleth. tmap is a dedicated thematic mapping package and leaflet builds interactive web maps. Coordinate reference systems must match, and st_transform() reprojects layers when they do not.
21

How do you compare means across more than two groups in R?

Save to ▾
Show answer
Use one-way ANOVA, then a post-hoc test to see which pairs differ. fit <- aov(sales ~ region, data = stores) summary(fit) # overall F-test TukeyHSD(fit) # pairwise comparisons # check assumptions plot(fit, which = 2) # Q-Q of residuals bartlett.test(sales ~ region, data = stores) # non-parametric alternative kruskal.test(sales ~ region, data = stores)
22

Product wants to detect a lift in conversion from 5 to 5.5 percent. How many users do you need per group?

Save to ▾
Show answer
I use power analysis before the test starts. In R, power.prop.test(p1 = 0.05, p2 = 0.055, power = 0.8, sig.level = 0.05) gives roughly 31,000 users per group. Smaller lifts need far larger samples, since sample size scales with the inverse square of the effect. I would then convert the sample into test duration using daily traffic, round up to full weeks, and agree on the stopping rule in advance to avoid peeking bias.
23

What is the multiple testing problem and how do you correct for it?

Save to ▾
Show answer
When you run many hypothesis tests, the chance of at least one false positive rises sharply. With 20 independent tests at alpha 0.05, it is about 64 percent. Corrections adjust p-values: Bonferroni multiplies by the number of tests and is conservative, Holm is a less strict step-down version, and Benjamini-Hochberg controls the false discovery rate. In R, p.adjust(pvals, method = 'BH') applies them, which is common when testing many metrics or segments.
24

What is bootstrapping and when would you use it?

Save to ▾
Show answer
Bootstrapping estimates the sampling distribution of a statistic by resampling the data with replacement many times and recomputing the statistic each time. The spread of those results gives standard errors and confidence intervals without assuming normality. It is useful for medians, ratios or complex metrics like revenue per user, where formulas are hard. In R, the boot package or a simple replicate() loop with sample(x, replace = TRUE) does it.
25

A manager says a test with p = 0.06 proves the change had no effect. How do you respond?

Save to ▾
Show answer
I explain that failing to reject the null is not proof of no effect. A p-value of 0.06 means the evidence is weak or the test was underpowered, and the 0.05 cut-off is a convention, not a law. I would show the confidence interval for the effect, which probably ranges from slightly negative to clearly positive, check whether the sample size met the planned power and suggest extending the test or weighing the decision on cost and risk.
26

You are analysing a national survey where some groups were deliberately oversampled. How do you compute correct averages?

Save to ▾
Show answer
Simple averages would be biased toward oversampled groups, so I use the survey weights provided with the data. In R, the survey package defines the design with svydesign(ids = ~psu, strata = ~stratum, weights = ~wt, data = df), then svymean(), svytotal() and svyglm() give weighted estimates with correct standard errors that account for clustering and stratification. The srvyr package offers the same with dplyr-style syntax.
27

Overall, treatment A has a higher success rate than B, but B is better in every hospital. How is that possible?

Save to ▾
Show answer
This is Simpson's paradox. It happens when a confounding variable, here hospital or patient severity, is unevenly distributed between groups. If A was used mostly in hospitals with easier cases, its overall rate looks better even though B wins within each hospital. In R I would group by hospital and compare rates, then use a stratified analysis or regression with hospital as a covariate to estimate the adjusted effect.
28

Tell me about a time your analysis did not support what a stakeholder hoped to see.

Save to ▾
Show answer
Situation: a marketing head expected a new campaign to have doubled sign-ups. Task: evaluate its impact. Action: my analysis in R using a pre-post comparison with a control region showed only a 6 percent lift, much of it seasonal. I presented the method and confidence interval openly, acknowledged what went well and suggested a cleaner randomized test. Result: the stakeholder accepted the finding, and the next campaign was run as a proper experiment.
29

What are closures and function factories in R?

Save to ▾
Show answer
A closure is a function that remembers the environment in which it was created. A function factory is a function that returns such functions. For example, power <- function(exp) function(x) x^exp; square <- power(2) creates a function that remembers exp = 2. They are useful for creating families of related functions, counters and memoised functions. Because of lazy evaluation, call force(exp) inside the factory so the value is captured immediately.
30

A loop calls an external API 5,000 times and crashes at call 3,200. How do you make it robust?

Save to ▾
Show answer
I wrap each call with purrr::safely() or tryCatch() so one failure does not stop the loop, and add retries with exponential backoff, which httr2 supports through req_retry(). I respect rate limits with Sys.sleep() or req_throttle(). I save results incrementally, for example every 100 calls to an RDS file or database, so a restart resumes from the last checkpoint. Finally I log failed IDs and rerun only those.
31

How do you run R code in parallel?

Save to ▾
Show answer
The parallel package ships with R: mclapply() forks processes on Linux and macOS, while parLapply() uses a socket cluster that also works on Windows. The future framework gives a cleaner interface where you choose a plan, such as plan(multisession), and then use future_map() from furrr or future_lapply(). Parallelism helps when each task is independent and heavy, such as model fitting per group, but adds overhead for small tasks and multiplies memory use.
32

A simulation loop runs for 40 minutes. How would you speed it up?

Save to ▾
Show answer
First I profile with profvis to find the slow lines. Then I try vectorizing, for example generating all random numbers at once with rnorm(n * reps) and using matrix operations instead of nested loops. I preallocate outputs instead of growing them. If iterations are independent, I run them in parallel with future and furrr. If a tight loop cannot be vectorized, I rewrite it in C++ with Rcpp, which often gives a 10 to 100 times speed-up.
33

What is Rcpp and when would you use it?

Save to ▾
Show answer
Rcpp is a package that lets you write C++ functions and call them from R with almost no boilerplate, using cppFunction() for small snippets or sourceCpp() for files. It is used when R code has loops that cannot be vectorized, such as recursive calculations, simulations with dependence between steps or custom algorithms. Many popular packages, including dplyr and data.table internals, rely on compiled code. Use it only after profiling shows a real bottleneck.
34

How do you use Reduce(), Filter() and Map() in base R?

Save to ▾
Show answer
These higher-order functions come from functional programming. nums <- 1:10 Filter(function(x) x %% 2 == 0, nums) # even numbers Map(function(a, b) a + b, 1:3, 4:6) # list: 5 7 9 Reduce(`+`, 1:5, accumulate = TRUE) # 1 3 6 10 15 # join many data frames on a common key dfs <- list(sales, targets, regions) Reduce(function(x, y) merge(x, y, by = 'region_id'), dfs) # purrr equivalents: keep(), map2(), reduce()
35

A function that looks up exchange rates is called thousands of times with the same inputs and is slow. How do you fix it?

Save to ▾
Show answer
The function repeats expensive work for identical inputs, so I would cache its results with memoisation. The memoise package wraps a function, memo_rate <- memoise::memoise(get_rate), and stores results by argument values, returning cached answers instantly. I can set a cache that expires, for example daily for rates, or store it on disk with cachem so it survives sessions. Alternatively I fetch all needed rates once and join them as a lookup table.
36

A predictor such as pincode has 500 levels. How do you use it in a model?

Save to ▾
Show answer
Dummy-encoding 500 levels creates sparse, noisy features and overfits rare levels. I first lump rare levels into 'other' with step_other(). Then I consider target or effect encoding with the embed package, applied inside cross-validation to avoid leakage, or mapping pincodes to meaningful higher-level features such as city tier, region or distance to a warehouse. Tree models in some engines handle many levels better, but grouping by business meaning usually works best.
37

You are asked to forecast daily demand for the Diwali season. How do you approach it?

Save to ▾
Show answer
I gather at least two or three years of daily history, since festive peaks happen once a year and Diwali moves across October and November. I add holiday regressors for Diwali, Dussehra and sale events rather than relying on calendar seasonality alone. I compare models like ETS, ARIMA with regressors and prophet in fable, validating on past festive seasons. I report prediction intervals and review with business teams for planned promotions or stock limits.
38

What is regularization and how do you apply it in R?

Save to ▾
Show answer
Regularization adds a penalty on coefficient size to reduce overfitting. Ridge (L2) shrinks all coefficients toward zero, useful with correlated predictors. Lasso (L1) can shrink some to exactly zero, performing feature selection. Elastic net mixes both. In R, glmnet fits these models, with alpha = 0 for ridge and alpha = 1 for lasso, and cv.glmnet() chooses the penalty lambda by cross-validation. Predictors should be standardized, which glmnet does by default.
39

Business users do not trust your gradient boosting model because it is a black box. What do you do?

Save to ▾
Show answer
I explain both global and individual behaviour. Globally, I show variable importance and partial dependence plots, using DALEX or the vip package, to show how key features affect predictions. For individual cases I use SHAP values, for example through the shapviz or fastshap packages, so a manager can see why a customer was flagged. I compare against a simple logistic model and walk users through real examples they know.
40

Your R model must score new customers every morning for the CRM team. How would you deploy it?

Save to ▾
Show answer
I save the fitted workflow, including the recipe, with saveRDS() or the vetiver package, which versions models and adds monitoring. For batch scoring, a scheduled R script reads new customers from the database, predicts and writes scores back, run by cron, Posit Connect or a container in the cloud. For real-time needs, I expose the model as a REST API with plumber or vetiver in Docker. I log inputs and track drift.
41

Tell me about a time a model you built did not perform as expected after deployment.

Save to ▾
Show answer
Situation: a demand model I built performed well in testing but its errors doubled two months after launch. Task: diagnose and fix it. Action: I compared input distributions and found a new product category and a pricing change that the training data never saw. I added drift monitoring, retrained with recent data and set up monthly retraining. Result: accuracy returned to target, and I learned to plan monitoring before deployment, not after.
42

Your Shiny dashboard becomes very slow when 50 users open it at once. How do you improve it?

Save to ▾
Show answer
I profile with profvis and reactlog to find slow reactives. I move heavy data loading outside the server function so it runs once per process, pre-aggregate data instead of computing on raw rows, and cache results with bindCache(). I use bindEvent() so expensive work runs only on a button click. Long computations can go to background tasks with ExtendedTask and promises. Finally I scale out with multiple R processes on Posit Connect or Shiny Server Pro.
43

What is the difference between reactive(), observe() and observeEvent() in Shiny?

Save to ▾
Show answer
reactive() creates a cached value that other code can read, such as filtered data, and it is lazy, recomputing only when needed. observe() runs code for its side effects whenever any reactive dependency changes, returning nothing. observeEvent() runs side effects only when a specific event fires, such as a button click, ignoring other dependencies. Use reactive() for computing values and observeEvent() for actions like saving data or showing notifications.
44

Your R pipeline works on your laptop but fails on the Linux server because of missing system libraries. How do you fix it permanently?

Save to ▾
Show answer
Packages like sf, xml2 or RPostgres need system libraries such as GDAL, libxml2 or libpq, which differ between machines. I would containerize the pipeline with Docker, starting from a rocker image like rocker/r-ver pinned to the R version, installing system dependencies in the Dockerfile and restoring packages with renv. The same image then runs on laptops, servers and the cloud, eliminating environment differences between development and production.
45

Tell me about a time a production job failed and you had to fix it under pressure.

Save to ▾
Show answer
Situation: the scheduled R job feeding the morning leadership dashboard failed at 6 AM after a source table added a new column type. Task: restore the dashboard before the 9 AM meeting. Action: I checked the logs, identified the parsing error, patched the column specification, reran the job and informed stakeholders of the delay. Afterwards I added schema validation and a failure alert. Result: data was live by 8:30 AM, and similar changes are now caught early.

Frequently asked questions

Who should practise hard R Programming questions?

Candidates targeting senior roles or top product companies, and anyone who finds the medium set easy.

How should I practise these?

Answer each question aloud before opening the answer, mark it as solved only when your answer matches the key idea, and save misses to Last-Minute Revision.

Other difficulty levels